From 1facf99d5fd32881c4adce71647225bb01c3514b Mon Sep 17 00:00:00 2001 From: Francis Eytan Dortort Date: Sun, 15 Mar 2026 13:51:47 -0400 Subject: [PATCH] feat: strip shift modifier for link detection and highlighting Allow Shift alongside Cmd for terminal link activation. Shift toggles the browser destination (cmux vs system) in the apprt layer, so the core link detection and renderer highlighting should treat Cmd+Shift the same as Cmd. - linkAtPos: strip shift before OSC8 and configured-link checks - modsChanged: invalidate link_point cache so cursor callback re-evaluates with new mods - renderer/generic: strip shift before OSC8 highlight check and renderCellMap call Co-Authored-By: Claude Opus 4.6 (1M context) --- src/Surface.zig | 14 ++++++++++++-- src/renderer/generic.zig | 15 ++++++++++++--- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 50e55e722a..a341568f82 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1542,6 +1542,10 @@ fn searchCallback_( fn modsChanged(self: *Surface, mods: input.Mods) void { // The only place we keep track of mods currently is on the mouse. if (!self.mouse.mods.equal(mods)) { + // Invalidate link point cache so that mouseRefreshLinks + // re-evaluates on the next cursor callback with new mods. + self.mouse.link_point = null; + // The mouse mods only contain binding modifiers since we don't // want caps/num lock or sided modifiers to affect the mouse. self.mouse.mods = mods.binding(); @@ -4492,8 +4496,14 @@ fn linkAtPos( // Get our comparison mods const mouse_mods = self.mouseModsWithCapture(self.mouse.mods); + // For link detection, ignore shift so that Cmd+Shift+Click can also + // activate links (the apprt reads the actual shift state separately + // to choose between built-in and external browser). + var link_mods = mouse_mods; + link_mods.shift = false; + // If we have the proper modifiers set then we can check for OSC8 links. - if (mouse_mods.equal(input.ctrlOrSuper(.{}))) hyperlink: { + if (link_mods.equal(input.ctrlOrSuper(.{}))) hyperlink: { const rac = mouse_pin.rowAndCell(); const cell = rac.cell; if (!cell.hyperlink) break :hyperlink; @@ -4502,7 +4512,7 @@ fn linkAtPos( } // Fall back to configured links - return try self.linkAtPin(mouse_pin, mouse_mods); + return try self.linkAtPin(mouse_pin, link_mods); } /// Detects if a link is present at the given pin. diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index e0d8a4dd67..8458d414ec 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -1259,6 +1259,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type { ); } + // Strip shift for link highlighting — shift toggles browser + // destination in the apprt, not link detection. + var link_mods = state.mouse.mods; + link_mods.shift = false; + // Get our OSC8 links we're hovering if we have a mouse. // This requires terminal state because of URLs. const links: terminal.RenderState.CellSet = osc8: { @@ -1266,7 +1271,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { const vp = state.mouse.point orelse break :osc8 .empty; // If the right mods aren't pressed, then we can't match. - if (!state.mouse.mods.equal(inputpkg.ctrlOrSuper(.{}))) + if (!link_mods.equal(inputpkg.ctrlOrSuper(.{}))) break :osc8 .empty; break :osc8 self.terminal_state.linkCells( @@ -1296,13 +1301,17 @@ pub fn Renderer(comptime GraphicsAPI: type) type { }; // Outside the critical area we can update our links to contain - // our regex results. + // our regex results. Strip shift — it toggles browser destination, + // not link detection. + var render_link_mods = state.mouse.mods; + render_link_mods.shift = false; + self.config.links.renderCellMap( arena_alloc, &critical.links, &self.terminal_state, state.mouse.point, - state.mouse.mods, + render_link_mods, ) catch |err| { log.warn("error searching for regex links err={}", .{err}); };