feat: Select/Copy Links On Right Click If Present (#9298)

This is a solution for
https://github.com/ghostty-org/ghostty/issues/2107.

**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.

When a user right-clicks, and there's no existing selection, the
existing behavior is to try to select the word under the cursor:


3548acfac6/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 https://github.com/ghostty-org/ghostty/issues/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).


3548acfac6/src/Surface.zig (L3896-L3901)

It also therefore respects `link-url` from config.


3548acfac6/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 no
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:

3548acfac6/src/Surface.zig (L3702-L3704)

**Original Behavior:**
(first without ctrl, then with ctrl)


https://github.com/user-attachments/assets/f9236c44-bea4-4be8-a54b-24d5ae24b2e7

**New Behavior:**
(first without ctrl, then with ctrl, then pasting)


https://github.com/user-attachments/assets/1e7fa1a9-236e-471d-9504-c820c68600bb
This commit is contained in:
Mitchell Hashimoto
2026-01-20 09:30:25 -08:00
committed by GitHub

View File

@@ -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.