From 2587a2efb44038e2b02aa1c97cfc5d98e65dafe4 Mon Sep 17 00:00:00 2001 From: Mike Kasberg Date: Tue, 21 Oct 2025 17:25:54 -0600 Subject: [PATCH] feat: Select/Copy Links On Right Click If Present This is a solution for #2107. When a user right-clicks, and there's no existing selection, the existing behavior is to try to select the word under the cursor: https://github.com/ghostty-org/ghostty/blob/3548acfac63e7674b5e25896f6b393474fe8ea65/src/Surface.zig#L3740-L3742 This PR tweaks that behavior _slightly_: If there's a link under our cursor, as determined by `linkAtPos`, select the link (to copy with the right-click context menu). Otherwise, select the word as before. As noted in #2107, this matches the behavior of iTerm and Gnome Terminal. It's worth noting that `linkAtPos` already does the right thing in terms of checking the links from config and their highlight/hover states (modified by Ctrl or Super depending on platform). https://github.com/ghostty-org/ghostty/blob/3548acfac63e7674b5e25896f6b393474fe8ea65/src/Surface.zig#L3896-L3901 It also therefore respects `link-url` from config. https://github.com/ghostty-org/ghostty/blob/3548acfac63e7674b5e25896f6b393474fe8ea65/src/config/Config.zig#L3411-L3416 By using `linkAtPos`, we get all that behavior for free. In practical terms, that means: - If I'm holding Ctrl so a link is underlined and I right click on it, it selects the underlined link. - If I'm not holding Ctrl and I right click on a link that is not underlined, it selects the word as before. - This behavior respects per-platform key bindings and user config settings. `linkAtPos` requires that the render state mutex is held. I believe it's safe to call because we're inside a block holding the mutex: https://github.com/ghostty-org/ghostty/blob/3548acfac63e7674b5e25896f6b393474fe8ea65/src/Surface.zig#L3702-L3704 **AI Disclosure:** I used Gemini CLI to help me with this PR because while I have many years of programming experience, this is my first time writing Zig. I prototyped a couple different approaches with AI before landing on this one, so AI generated various prototypes and I chose the final imlementation. I've verified that my code compiles and works as intended. --- src/Surface.zig | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 4103b91fb..1f50cb681 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -4225,8 +4225,8 @@ pub fn mouseButtonCallback( // Get our viewport pin const screen: *terminal.Screen = self.renderer_state.terminal.screens.active; + const pos = try self.rt_surface.getCursorPos(); 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 = .{ @@ -4257,8 +4257,14 @@ pub fn mouseButtonCallback( // word selection where we clicked. } - const sel = screen.selectWord(pin) orelse break :sel; - try self.setSelection(sel); + // If there is a link at this position, we want to + // select the link. Otherwise, select the word. + if (try self.linkAtPos(pos)) |link| { + try self.setSelection(link.selection); + } else { + const sel = screen.selectWord(pin) orelse break :sel; + try self.setSelection(sel); + } try self.queueRender(); // Don't consume so that we show the context menu in apprt.