macos: left mouse click while not focused doesn't encode to pty (#6998)

Fixes #2595

This fixes an issue where a left mouse click on a terminal while not
focused would subsequently be encoded to the pty as a mouse event. This
is atypical for macOS applications in general and wasn't something we
wanted to do.

We do, however, want to ensure our terminal gains focus when clicked
without focus. Specifically, a split. This matches iTerm2 behavior and
is rather nice. We had this behavior before but our logic to make this
work before caused the issue this commit is fixing.

I also tested this with command+click which is a common macOS shortcut
to emit a mouse event without raising the focus of the target window. In
this case, we will properly focus the split but will not encode the
mouse event to the pty. I think we actually do a _better job_ here tha
iTerm2 (but, subjective) because we do encode the pty event properly if
the split is focused whereas iTerm2 never does.
This commit is contained in:
Mitchell Hashimoto
2025-04-04 19:31:22 -04:00
committed by GitHub

View File

@ -201,7 +201,14 @@ extension Ghostty {
self.eventMonitor = NSEvent.addLocalMonitorForEvents(
matching: [
// We need keyUp because command+key events don't trigger keyUp.
.keyUp
.keyUp,
// We need leftMouseDown to determine if we should focus ourselves
// when the app/window isn't in focus. We do this instead of
// "acceptsFirstMouse" because that forces us to also handle the
// event and encode the event to the pty which we want to avoid.
// (Issue 2595)
.leftMouseDown,
]
) { [weak self] event in self?.localEventHandler(event) }
@ -450,11 +457,40 @@ extension Ghostty {
case .keyUp:
localEventKeyUp(event)
case .leftMouseDown:
localEventLeftMouseDown(event)
default:
event
}
}
private func localEventLeftMouseDown(_ event: NSEvent) -> NSEvent? {
// We only want to process events that are on this window.
guard let window,
event.window != nil,
window == event.window else { return event }
// The clicked location in this window should be this view.
let location = convert(event.locationInWindow, from: nil)
guard hitTest(location) == self else { return event }
// We only want to grab focus if either our app or window was
// not focused.
guard !NSApp.isActive || !window.isKeyWindow else { return event }
// If we're already focused we do nothing
guard !focused else { return event }
// Make ourselves the first responder
window.makeFirstResponder(self)
// We have to keep processing the event so that AppKit can properly
// focus the window and dispatch events. If you return nil here then
// nobody gets a windowDidBecomeKey event and so on.
return event
}
private func localEventKeyUp(_ event: NSEvent) -> NSEvent? {
// We only care about events with "command" because all others will
// trigger the normal responder chain.
@ -620,14 +656,6 @@ extension Ghostty {
ghostty_surface_draw(surface);
}
override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
// "Override this method in a subclass to allow instances to respond to
// click-through. This allows the user to click on a view in an inactive
// window, activating the view with one click, instead of clicking first
// to make the window active and then clicking the view."
return true
}
override func mouseDown(with event: NSEvent) {
guard let surface = self.surface else { return }
let mods = Ghostty.ghosttyMods(event.modifierFlags)