From 03f37087a5e24a76b02404da439ccc6fb7f3da76 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 30 Jun 2024 09:40:06 -0700 Subject: [PATCH 01/10] mouse button callbacks returns bool for consumption --- include/ghostty.h | 2 +- .../Sources/Ghostty/SurfaceView_AppKit.swift | 37 +++++++++++++++++-- src/Surface.zig | 19 ++++++---- src/apprt/embedded.zig | 10 ++--- src/apprt/glfw.zig | 2 +- 5 files changed, 52 insertions(+), 18 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index e3a9e4223..2f1ce04a3 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -533,7 +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); -void ghostty_surface_mouse_button(ghostty_surface_t, +bool ghostty_surface_mouse_button(ghostty_surface_t, ghostty_input_mouse_state_e, ghostty_input_mouse_button_e, ghostty_input_mods_e); diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index f41a01d57..47641875a 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -471,15 +471,39 @@ extension Ghostty { override func rightMouseDown(with event: NSEvent) { - guard let surface = self.surface else { return } + guard let surface = self.surface else { return super.rightMouseDown(with: event) } + let mods = Ghostty.ghosttyMods(event.modifierFlags) - ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_PRESS, GHOSTTY_MOUSE_RIGHT, mods) + if (ghostty_surface_mouse_button( + surface, + GHOSTTY_MOUSE_PRESS, + GHOSTTY_MOUSE_RIGHT, + mods + )) { + // Consumed + return + } + + // Mouse event not consumed + super.rightMouseDown(with: event) } override func rightMouseUp(with event: NSEvent) { - guard let surface = self.surface else { return } + guard let surface = self.surface else { return super.rightMouseUp(with: event) } + let mods = Ghostty.ghosttyMods(event.modifierFlags) - ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_RELEASE, GHOSTTY_MOUSE_RIGHT, mods) + if (ghostty_surface_mouse_button( + surface, + GHOSTTY_MOUSE_RELEASE, + GHOSTTY_MOUSE_RIGHT, + mods + )) { + // Handled + return + } + + // Mouse event not consumed + super.rightMouseUp(with: event) } override func mouseMoved(with event: NSEvent) { @@ -842,6 +866,11 @@ extension Ghostty { ghostty_surface_key(surface, key_ev) } } + + override func menu(for event: NSEvent) -> NSMenu? { + Ghostty.logger.warning("menu: event!") + return nil + } // MARK: Menu Handlers diff --git a/src/Surface.zig b/src/Surface.zig index a4557be78..34042c2e2 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -2129,12 +2129,15 @@ fn mouseShiftCapture(self: *const Surface, lock: bool) bool { }; } +/// 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. pub fn mouseButtonCallback( self: *Surface, action: input.MouseButtonState, button: input.MouseButton, mods: input.Mods, -) !void { +) !bool { // log.debug("mouse action={} button={} mods={}", .{ action, button, mods }); // If we have an inspector, we always queue a render @@ -2155,7 +2158,7 @@ pub fn mouseButtonCallback( const screen = &self.renderer_state.terminal.screen; const p = screen.pages.pin(.{ .viewport = point }) orelse { log.warn("failed to get pin for clicked point", .{}); - return; + return false; }; insp.cell.select( @@ -2166,7 +2169,7 @@ pub fn mouseButtonCallback( ) catch |err| { log.warn("error selecting cell for inspector err={}", .{err}); }; - return; + return false; } } @@ -2205,7 +2208,7 @@ pub fn mouseButtonCallback( if (selection) { const pos = try self.rt_surface.getCursorPos(); try self.cursorPosCallback(pos); - return; + return true; } } } @@ -2216,7 +2219,7 @@ pub fn mouseButtonCallback( if (button == .left and action == .release and self.mouse.over_link) { const pos = try self.rt_surface.getCursorPos(); if (self.processLinks(pos)) |processed| { - if (processed) return; + if (processed) return true; } else |err| { log.warn("error processing links err={}", .{err}); } @@ -2257,7 +2260,7 @@ pub fn mouseButtonCallback( // If we're doing mouse reporting, we do not support any other // selection or highlighting. - return; + return true; } } @@ -2269,7 +2272,7 @@ pub fn mouseButtonCallback( self.renderer_state.mutex.lock(); defer self.renderer_state.mutex.unlock(); try self.clickMoveCursor(pin.*); - return; + return true; } // For left button clicks we always record some information for @@ -2398,6 +2401,8 @@ pub fn mouseButtonCallback( try self.startClipboardRequest(clipboard, .{ .paste = {} }); } } + + return false; } /// Performs the "click-to-move" logic to move the cursor to the given diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 0ab955206..8507b9835 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -710,10 +710,10 @@ pub const Surface = struct { action: input.MouseButtonState, button: input.MouseButton, mods: input.Mods, - ) void { - self.core_surface.mouseButtonCallback(action, button, mods) catch |err| { + ) bool { + return self.core_surface.mouseButtonCallback(action, button, mods) catch |err| { log.err("error in mouse button callback err={}", .{err}); - return; + return false; }; } @@ -1638,8 +1638,8 @@ pub const CAPI = struct { action: input.MouseButtonState, button: input.MouseButton, mods: c_int, - ) void { - surface.mouseButtonCallback( + ) bool { + return surface.mouseButtonCallback( action, button, @bitCast(@as( diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 932e27de5..911eb6f5e 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -1048,7 +1048,7 @@ pub const Surface = struct { else => unreachable, }; - core_win.mouseButtonCallback(action, button, mods) catch |err| { + _ = core_win.mouseButtonCallback(action, button, mods) catch |err| { log.err("error in scroll callback err={}", .{err}); return; }; From a771d6573522f3f4630160ef85db153556048720 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 30 Jun 2024 10:15:36 -0700 Subject: [PATCH 02/10] macos: start context-menu --- .../Sources/Ghostty/SurfaceView_AppKit.swift | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 47641875a..371d6330e 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -868,10 +868,26 @@ extension Ghostty { } override func menu(for event: NSEvent) -> NSMenu? { - Ghostty.logger.warning("menu: event!") - return nil - } + // We only support right-click menus + guard event.type == .rightMouseDown else { return nil } + + // We need a surface + guard let surface = self.surface else { return nil } + + let menu = NSMenu() + + // If we have a selection, add copy + if ghostty_surface_has_selection(surface) { + menu.addItem(withTitle: "Copy", action: #selector(copy(_:)), keyEquivalent: "") + } + menu.addItem(withTitle: "Paste", action: #selector(paste(_:)), keyEquivalent: "") + + menu.addItem(NSMenuItem.separator()) + menu.addItem(withTitle: "Toggle Terminal Inspector", action: #selector(TerminalController.toggleTerminalInspector(_:)), keyEquivalent: "") + return menu + } + // MARK: Menu Handlers @IBAction func copy(_ sender: Any?) { From a586eb988913e5707b22d8447385ad79161698ad Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 30 Jun 2024 10:22:34 -0700 Subject: [PATCH 03/10] core: right-click press selects word --- src/Surface.zig | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/Surface.zig b/src/Surface.zig index 34042c2e2..5573f6ef6 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -2402,6 +2402,42 @@ pub fn mouseButtonCallback( } } + // Right-click down selects word for context menus. If the apprt + // doesn't implement context menus this can be a bit weird but they + // are supported by our two main apprts so we always do this. If we + // want to be careful in the future we can add a function to apprts + // that let's us know. + if (button == .right and action == .press) sel: { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + + // Get our viewport pin + const screen = &self.renderer_state.terminal.screen; + const pin = pin: { + const pos = try self.rt_surface.getCursorPos(); + const pt_viewport = self.posToViewport(pos.x, pos.y); + const pin = screen.pages.pin(.{ + .viewport = .{ + .x = pt_viewport.x, + .y = pt_viewport.y, + }, + }) orelse { + // Weird... our viewport x/y that we just converted isn't + // found in our pages. This is probably a bug but we don't + // want to crash in releases because its harmless. So, we + // only assert in debug mode. + if (comptime std.debug.runtime_safety) unreachable; + break :sel; + }; + + break :pin pin; + }; + + const sel = screen.selectWord(pin) orelse break :sel; + try self.setSelection(sel); + try self.queueRender(); + } + return false; } From 32588a647f8153d13848cac49a55436ed5d4c725 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 30 Jun 2024 10:24:53 -0700 Subject: [PATCH 04/10] core: on right click, only create selection if point isn't in prev sel --- src/Surface.zig | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Surface.zig b/src/Surface.zig index 5573f6ef6..9d670cf23 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -2433,6 +2433,15 @@ pub fn mouseButtonCallback( break :pin pin; }; + // If we already have a selection and the selection contains + // where we clicked then we don't want to modify the selection. + if (self.io.terminal.screen.selection) |prev_sel| { + if (prev_sel.contains(screen, pin)) break :sel; + + // The selection doesn't contain our pin, so we create a new + // word selection where we clicked. + } + const sel = screen.selectWord(pin) orelse break :sel; try self.setSelection(sel); try self.queueRender(); From 27fd05d112e11933f1cf7d9864ca13cbff8d72a8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 30 Jun 2024 10:26:27 -0700 Subject: [PATCH 05/10] apprt/gtk: conform to new mouse button API --- src/apprt/gtk/Surface.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 57bbf0d38..1ee433db9 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -1164,7 +1164,7 @@ fn gtkMouseDown( self.grabFocus(); } - self.core_surface.mouseButtonCallback(.press, button, mods) catch |err| { + _ = self.core_surface.mouseButtonCallback(.press, button, mods) catch |err| { log.err("error in key callback err={}", .{err}); return; }; @@ -1184,7 +1184,7 @@ fn gtkMouseUp( const mods = translateMods(gtk_mods); const self = userdataSelf(ud.?); - self.core_surface.mouseButtonCallback(.release, button, mods) catch |err| { + _ = self.core_surface.mouseButtonCallback(.release, button, mods) catch |err| { log.err("error in key callback err={}", .{err}); return; }; From 6c01d20eb775785fa4cd446b91e304cabc1fdb45 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 30 Jun 2024 14:58:28 -0700 Subject: [PATCH 06/10] macos: add string extension --- macos/Ghostty.xcodeproj/project.pbxproj | 6 ++++++ macos/Sources/Helpers/String+Extension.swift | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 macos/Sources/Helpers/String+Extension.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index fa0b5e263..0b5af9251 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -50,6 +50,8 @@ A59630A02AEF6AEB00D64628 /* TerminalManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A596309F2AEF6AEB00D64628 /* TerminalManager.swift */; }; A59630A22AF0415000D64628 /* Ghostty.TerminalSplit.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59630A12AF0415000D64628 /* Ghostty.TerminalSplit.swift */; }; A59630A42AF059BB00D64628 /* Ghostty.SplitNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59630A32AF059BB00D64628 /* Ghostty.SplitNode.swift */; }; + A5985CD72C320C4500C57AD3 /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5985CD62C320C4500C57AD3 /* String+Extension.swift */; }; + A5985CD82C320C4500C57AD3 /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5985CD62C320C4500C57AD3 /* String+Extension.swift */; }; A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */; }; A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59FB5D02AE0DEA7009128F3 /* MetalView.swift */; }; A5A1F8852A489D6800D1E8BC /* terminfo in Resources */ = {isa = PBXBuildFile; fileRef = A5A1F8842A489D6800D1E8BC /* terminfo */; }; @@ -111,6 +113,7 @@ A596309F2AEF6AEB00D64628 /* TerminalManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalManager.swift; sourceTree = ""; }; A59630A12AF0415000D64628 /* Ghostty.TerminalSplit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.TerminalSplit.swift; sourceTree = ""; }; A59630A32AF059BB00D64628 /* Ghostty.SplitNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.SplitNode.swift; sourceTree = ""; }; + A5985CD62C320C4500C57AD3 /* String+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Extension.swift"; sourceTree = ""; }; A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorView.swift; sourceTree = ""; }; A59FB5D02AE0DEA7009128F3 /* MetalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetalView.swift; sourceTree = ""; }; A5A1F8842A489D6800D1E8BC /* terminfo */ = {isa = PBXFileReference; lastKnownFileType = folder; name = terminfo; path = "../zig-out/share/terminfo"; sourceTree = ""; }; @@ -205,6 +208,7 @@ C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */, C1F26EA62B738B9900404083 /* NSView+Extension.swift */, AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */, + A5985CD62C320C4500C57AD3 /* String+Extension.swift */, C1F26EE72B76CBFC00404083 /* VibrantLayer.h */, C1F26EE82B76CBFC00404083 /* VibrantLayer.m */, A5CEAFDA29B8005900646FDA /* SplitView */, @@ -503,6 +507,7 @@ A5333E1C2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */, A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */, A56D58862ACDDB4100508D2C /* Ghostty.Shell.swift in Sources */, + A5985CD72C320C4500C57AD3 /* String+Extension.swift in Sources */, A59630A22AF0415000D64628 /* Ghostty.TerminalSplit.swift in Sources */, A5FEB3002ABB69450068369E /* main.swift in Sources */, A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */, @@ -537,6 +542,7 @@ A53D0C9C2B543F7B00305CE6 /* Package.swift in Sources */, A53D0C9B2B543F3B00305CE6 /* Ghostty.App.swift in Sources */, A5333E242B5A22D9008AEFF7 /* Ghostty.Shell.swift in Sources */, + A5985CD82C320C4500C57AD3 /* String+Extension.swift in Sources */, C159E89D2B69A2EF00FDFE9C /* OSColor+Extension.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/macos/Sources/Helpers/String+Extension.swift b/macos/Sources/Helpers/String+Extension.swift new file mode 100644 index 000000000..54601b231 --- /dev/null +++ b/macos/Sources/Helpers/String+Extension.swift @@ -0,0 +1,18 @@ +extension String { + func truncate(length: Int, trailing: String = "…") -> String { + let maxLength = length - trailing.count + guard maxLength > 0, !self.isEmpty, self.count > length else { + return self + } + return self.prefix(maxLength) + trailing + } + + func temporaryFile(_ filename: String = "temp") -> URL { + let url = FileManager.default.temporaryDirectory + .appendingPathComponent(filename) + .appendingPathExtension("txt") + let string = self + try? string.write(to: url, atomically: true, encoding: .utf8) + return url + } +} From 174d83bfd3436b440664737b10e32955bcb33d18 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 30 Jun 2024 15:18:33 -0700 Subject: [PATCH 07/10] macos: add windowing commands to context menu --- macos/Sources/Ghostty/SurfaceView_AppKit.swift | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 371d6330e..4a4b5fbda 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -871,18 +871,23 @@ extension Ghostty { // We only support right-click menus guard event.type == .rightMouseDown else { return nil } - // We need a surface - guard let surface = self.surface else { return nil } - let menu = NSMenu() + + // Windowing + menu.addItem(withTitle: "New Window", action: #selector(TerminalController.newWindow(_:)), keyEquivalent: "") + menu.addItem(withTitle: "New Tab", action: #selector(TerminalController.newTab(_:)), keyEquivalent: "") + menu.addItem(withTitle: "Split Right", action: #selector(TerminalController.splitRight(_:)), keyEquivalent: "") + menu.addItem(withTitle: "Split Down", action: #selector(TerminalController.splitDown(_:)), keyEquivalent: "") + menu.addItem(.separator()) + // If we have a selection, add copy - if ghostty_surface_has_selection(surface) { + if self.selectedRange().length > 0 { menu.addItem(withTitle: "Copy", action: #selector(copy(_:)), keyEquivalent: "") } menu.addItem(withTitle: "Paste", action: #selector(paste(_:)), keyEquivalent: "") - menu.addItem(NSMenuItem.separator()) + menu.addItem(.separator()) menu.addItem(withTitle: "Toggle Terminal Inspector", action: #selector(TerminalController.toggleTerminalInspector(_:)), keyEquivalent: "") return menu From f0737356cb51a31a39931cd7a095bafc2b62544d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 30 Jun 2024 15:19:59 -0700 Subject: [PATCH 08/10] macos: i don't like windowing in the context menu --- macos/Sources/Ghostty/SurfaceView_AppKit.swift | 8 -------- 1 file changed, 8 deletions(-) diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 4a4b5fbda..ea4c86dc5 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -872,14 +872,6 @@ extension Ghostty { guard event.type == .rightMouseDown else { return nil } let menu = NSMenu() - - // Windowing - menu.addItem(withTitle: "New Window", action: #selector(TerminalController.newWindow(_:)), keyEquivalent: "") - menu.addItem(withTitle: "New Tab", action: #selector(TerminalController.newTab(_:)), keyEquivalent: "") - menu.addItem(withTitle: "Split Right", action: #selector(TerminalController.splitRight(_:)), keyEquivalent: "") - menu.addItem(withTitle: "Split Down", action: #selector(TerminalController.splitDown(_:)), keyEquivalent: "") - menu.addItem(.separator()) - // If we have a selection, add copy if self.selectedRange().length > 0 { From 1d70e822c5e691a4962f31fb726eb236e225611b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 30 Jun 2024 19:03:06 -0700 Subject: [PATCH 09/10] macos: disable macOS-only API on iOS --- macos/Sources/Helpers/String+Extension.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/macos/Sources/Helpers/String+Extension.swift b/macos/Sources/Helpers/String+Extension.swift index 54601b231..f39ef8d3d 100644 --- a/macos/Sources/Helpers/String+Extension.swift +++ b/macos/Sources/Helpers/String+Extension.swift @@ -7,6 +7,7 @@ extension String { return self.prefix(maxLength) + trailing } + #if canImport(AppKit) func temporaryFile(_ filename: String = "temp") -> URL { let url = FileManager.default.temporaryDirectory .appendingPathComponent(filename) @@ -15,4 +16,5 @@ extension String { try? string.write(to: url, atomically: true, encoding: .utf8) return url } + #endif } From 57d71450ab0437018aee3da8b6e14bffcc95c89b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 30 Jun 2024 19:44:51 -0700 Subject: [PATCH 10/10] ctrl+click is handled as right-click (with various details) If mouse capturing is enabled, we encode ctrl+click as ctrl+left-click and DO NOT handle it as right click. --- include/ghostty.h | 1 + .../Sources/Ghostty/SurfaceView_AppKit.swift | 36 ++++++++++++++++++- src/Surface.zig | 8 +++++ src/apprt/embedded.zig | 6 ++++ 4 files changed, 50 insertions(+), 1 deletion(-) 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,