Skip to content

Commit 3855df9

Browse files
committed
Add focus event support gated by DEC mode 1004
Focus in/out events are now sent to the terminal when the application has enabled focus reporting via DECSET 1004. The Zig module checks the terminal mode before encoding, so focus events are silently ignored for shells that haven't requested them. Emacs focus-in-hook and focus-out-hook are connected in ghostel-mode and cleaned up in the process sentinel.
1 parent c8024ed commit 3855df9

4 files changed

Lines changed: 76 additions & 2 deletions

File tree

ghostel.el

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
(declare-function ghostel--scroll "ghostel-module")
3636
(declare-function ghostel--encode-key "ghostel-module")
3737
(declare-function ghostel--mouse-event "ghostel-module")
38+
(declare-function ghostel--focus-event "ghostel-module")
3839

3940
;;; Customization
4041

@@ -523,6 +524,26 @@ DIR may be a file:// URL or a plain path."
523524
(when (and path (file-directory-p path))
524525
(setq default-directory (file-name-as-directory path))))))
525526

527+
;;; Focus events
528+
529+
(defun ghostel--focus-in ()
530+
"Notify the terminal that Emacs gained focus.
531+
Only sends the event if the terminal has enabled focus reporting (mode 1004)."
532+
(when (and (eq major-mode 'ghostel-mode)
533+
ghostel--term
534+
ghostel--process
535+
(process-live-p ghostel--process))
536+
(ghostel--focus-event ghostel--term t)))
537+
538+
(defun ghostel--focus-out ()
539+
"Notify the terminal that Emacs lost focus.
540+
Only sends the event if the terminal has enabled focus reporting (mode 1004)."
541+
(when (and (eq major-mode 'ghostel-mode)
542+
ghostel--term
543+
ghostel--process
544+
(process-live-p ghostel--process))
545+
(ghostel--focus-event ghostel--term nil)))
546+
526547
;;; Process management
527548

528549
(defun ghostel--filter (process output)
@@ -545,6 +566,8 @@ PROCESS is the shell process."
545566
(when ghostel--redraw-timer
546567
(cancel-timer ghostel--redraw-timer)
547568
(setq ghostel--redraw-timer nil))
569+
(remove-hook 'focus-in-hook #'ghostel--focus-in)
570+
(remove-hook 'focus-out-hook #'ghostel--focus-out)
548571
(let ((inhibit-read-only t))
549572
(goto-char (point-max))
550573
(insert "\n[Process exited]\n")))))
@@ -636,7 +659,8 @@ PROCESS is the shell process, WINDOWS is the list of windows."
636659
(setq-local scroll-conservatively 101)
637660
(setq-local window-adjust-process-window-size-function
638661
#'ghostel--window-adjust-process-window-size)
639-
)
662+
(add-hook 'focus-in-hook #'ghostel--focus-in)
663+
(add-hook 'focus-out-hook #'ghostel--focus-out))
640664

641665
;;; Entry point
642666

src/module.zig

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,9 +242,17 @@ fn fnMouseEvent(raw_env: ?*c.emacs_env, _: isize, args: [*c]c.emacs_value, _: ?*
242242

243243
/// (ghostel--focus-event TERM GAINED)
244244
/// Encode a focus gained/lost event and send to the PTY.
245+
/// Only sends if the terminal has enabled focus reporting (DEC mode 1004).
245246
fn fnFocusEvent(raw_env: ?*c.emacs_env, _: isize, args: [*c]c.emacs_value, _: ?*anyopaque) callconv(.c) c.emacs_value {
246247
const env = emacs.Env.init(raw_env.?);
247-
_ = env.getUserPtr(Terminal, args[0]) orelse return env.nil();
248+
const term = env.getUserPtr(Terminal, args[0]) orelse return env.nil();
249+
250+
// Only send focus events if the terminal has enabled mode 1004
251+
// Construct mode value manually: DEC private mode 1004 = value & 0x7FFF, ansi=false (bit 15=0)
252+
const focus_mode: gt.c.GhosttyMode = 1004;
253+
if (!term.isModeEnabled(focus_mode)) {
254+
return env.nil();
255+
}
248256

249257
const gained = env.isNotNil(args[1]);
250258
const event: gt.c.GhosttyFocusEvent = if (gained) gt.c.GHOSTTY_FOCUS_GAINED else gt.c.GHOSTTY_FOCUS_LOST;
@@ -255,6 +263,10 @@ fn fnFocusEvent(raw_env: ?*c.emacs_env, _: isize, args: [*c]c.emacs_value, _: ?*
255263
return env.nil();
256264
}
257265

266+
// Stash env for the flush callback
267+
term.env = env;
268+
defer term.env = null;
269+
258270
_ = env.call1(env.intern("ghostel--flush-output"), env.makeString(buf[0..written]));
259271
return env.t();
260272
}

src/terminal.zig

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,15 @@ pub fn getPwd(self: *Self) ?[]const u8 {
194194
return pwd.ptr[0..pwd.len];
195195
}
196196

197+
/// Check if a terminal mode is enabled.
198+
pub fn isModeEnabled(self: *Self, mode: gt.c.GhosttyMode) bool {
199+
var enabled: bool = false;
200+
if (gt.c.ghostty_terminal_mode_get(self.terminal, mode, &enabled) != gt.SUCCESS) {
201+
return false;
202+
}
203+
return enabled;
204+
}
205+
197206
/// Emacs finalizer — called when the user-ptr is garbage collected.
198207
pub fn emacsFinalize(ptr: ?*anyopaque) callconv(.c) void {
199208
if (ptr) |p| {

test/ghostel-test.el

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,34 @@
360360
(ghostel--update-directory "file:///usr")
361361
(ghostel-test--assert-equal "dedup" old ghostel--last-directory))))
362362

363+
;; -----------------------------------------------------------------------
364+
;; Test: focus events gated by mode 1004
365+
;; -----------------------------------------------------------------------
366+
367+
(defun ghostel-test-focus-events ()
368+
"Test that focus events are only sent when mode 1004 is enabled."
369+
(message "--- focus events ---")
370+
(let ((term (ghostel--new 25 80 1000))
371+
(ghostel--flush-output-data nil))
372+
;; Without mode 1004 enabled, focus-event should return nil (not sent)
373+
(ghostel-test--assert-equal "focus ignored without mode 1004"
374+
nil
375+
(ghostel--focus-event term t))
376+
;; Enable mode 1004 via DECSET
377+
(ghostel--write-input term "\e[?1004h")
378+
;; Now focus-event should return t (sent)
379+
(ghostel-test--assert-equal "focus sent with mode 1004"
380+
t
381+
(ghostel--focus-event term t))
382+
(ghostel-test--assert-equal "focus-out sent with mode 1004"
383+
t
384+
(ghostel--focus-event term nil))
385+
;; Disable mode 1004 via DECRST
386+
(ghostel--write-input term "\e[?1004l")
387+
(ghostel-test--assert-equal "focus ignored after mode 1004 reset"
388+
nil
389+
(ghostel--focus-event term t))))
390+
363391
;; -----------------------------------------------------------------------
364392
;; Test: incremental (partial) redraw
365393
;; -----------------------------------------------------------------------
@@ -426,6 +454,7 @@
426454
(ghostel-test-title)
427455
(ghostel-test-crlf)
428456
(ghostel-test-incremental-redraw)
457+
(ghostel-test-focus-events)
429458

430459
;; Integration test (spawns a real shell)
431460
(ghostel-test-shell-integration)

0 commit comments

Comments
 (0)