diff --git a/include/ghostty.h b/include/ghostty.h index 2f1ce04a3..a082d1a6a 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -533,6 +533,7 @@ ghostty_input_mods_e ghostty_surface_key_translation_mods(ghostty_surface_t, ghostty_input_mods_e); void ghostty_surface_key(ghostty_surface_t, ghostty_input_key_s); void ghostty_surface_text(ghostty_surface_t, const char*, uintptr_t); +bool ghostty_surface_mouse_captured(ghostty_surface_t); bool ghostty_surface_mouse_button(ghostty_surface_t, ghostty_input_mouse_state_e, ghostty_input_mouse_button_e, diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index ea4c86dc5..499f85b0e 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -869,7 +869,41 @@ extension Ghostty { override func menu(for event: NSEvent) -> NSMenu? { // We only support right-click menus - guard event.type == .rightMouseDown else { return nil } + switch event.type { + case .rightMouseDown: + // Good + break + + case .leftMouseDown: + if !event.modifierFlags.contains(.control) { + return nil + } + + // In this case, AppKit calls menu BEFORE calling any mouse events. + // If mouse capturing is enabled then we never show the context menu + // so that we can handle ctrl+left-click in the terminal app. + guard let surface = self.surface else { return nil } + if ghostty_surface_mouse_captured(surface) { + return nil + } + + // If we return a non-nil menu then mouse events will never be + // processed by the core, so we need to manually send a right + // mouse down event. + // + // Note this never sounds a right mouse up event but that's the + // same as normal right-click with capturing disabled from AppKit. + let mods = Ghostty.ghosttyMods(event.modifierFlags) + ghostty_surface_mouse_button( + surface, + GHOSTTY_MOUSE_PRESS, + GHOSTTY_MOUSE_RIGHT, + mods + ) + + default: + return nil + } let menu = NSMenu() diff --git a/src/Surface.zig b/src/Surface.zig index 9d670cf23..a63c85cb2 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -2129,6 +2129,14 @@ fn mouseShiftCapture(self: *const Surface, lock: bool) bool { }; } +/// Returns true if the mouse is currently captured by the terminal +/// (i.e. reporting events). +pub fn mouseCaptured(self: *Surface) bool { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + return self.io.terminal.flags.mouse_event != .none; +} + /// Called for mouse button press/release events. This will return true /// if the mouse event was consumed in some way (i.e. the program is capturing /// mouse events). If the event was not consumed, then false is returned. diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 8507b9835..d1557eaa0 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -1632,6 +1632,12 @@ pub const CAPI = struct { surface.textCallback(ptr[0..len]); } + /// Returns true if the surface currently has mouse capturing + /// enabled. + export fn ghostty_surface_mouse_captured(surface: *Surface) bool { + return surface.core_surface.mouseCaptured(); + } + /// Tell the surface that it needs to schedule a render export fn ghostty_surface_mouse_button( surface: *Surface,