diff --git a/src/Surface.zig b/src/Surface.zig index 11fa42e35..7e9a307e5 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -327,7 +327,7 @@ const DerivedConfig = struct { window_width: u32, title: ?[:0]const u8, title_report: bool, - links: []Link, + links: []DerivedConfig.Link, link_previews: configpkg.LinkPreviews, scroll_to_bottom: configpkg.Config.ScrollToBottom, notify_on_command_finish: configpkg.Config.NotifyOnCommandFinish, @@ -347,7 +347,7 @@ const DerivedConfig = struct { // Build all of our links const links = links: { - var links: std.ArrayList(Link) = .empty; + var links: std.ArrayList(DerivedConfig.Link) = .empty; defer links.deinit(alloc); for (config.link.links.items) |link| { var regex = try link.oniRegex(); @@ -1599,10 +1599,10 @@ fn mouseRefreshLinks( } const link = (try self.linkAtPos(pos)) orelse break :link .{ null, false }; - switch (link[0]) { + switch (link.action) { .open => { const str = try self.io.terminal.screens.active.selectionString(alloc, .{ - .sel = link[1], + .sel = link.selection, .trim = false, }); break :link .{ @@ -1613,7 +1613,7 @@ fn mouseRefreshLinks( ._open_osc8 => { // Show the URL in the status bar - const pin = link[1].start(); + const pin = link.selection.start(); const uri = self.osc8URI(pin) orelse { log.warn("failed to get URI for OSC8 hyperlink", .{}); break :link .{ null, false }; @@ -4146,11 +4146,17 @@ pub fn mouseButtonCallback( 2 => { const sel_ = sel: { // Try link detection without requiring modifier keys - const link_result = self.linkAtPin(pin.*, null) catch null; - if (link_result) |result| { - // Only select URLs (links with .open action) - if (result[0] == .open) break :sel result[1]; + if (self.linkAtPin( + pin.*, + null, + )) |result_| { + if (result_) |result| { + break :sel result.selection; + } + } else |_| { + // Ignore any errors, likely regex errors. } + break :sel self.io.terminal.screens.active.selectWord(pin.*); }; if (sel_) |sel| { @@ -4340,16 +4346,18 @@ fn clickMoveCursor(self: *Surface, to: terminal.Pin) !void { } } +const Link = struct { + action: input.Link.Action, + selection: terminal.Selection, +}; + /// Returns the link at the given cursor position, if any. /// /// Requires the renderer mutex is held. fn linkAtPos( self: *Surface, pos: apprt.CursorPos, -) !?struct { - input.Link.Action, - terminal.Selection, -} { +) !?Link { // Convert our cursor position to a screen point. const screen: *terminal.Screen = self.renderer_state.terminal.screens.active; const mouse_pin: terminal.Pin = mouse_pin: { @@ -4370,29 +4378,27 @@ fn linkAtPos( const cell = rac.cell; if (!cell.hyperlink) break :hyperlink; const sel = terminal.Selection.init(mouse_pin, mouse_pin, false); - return .{ ._open_osc8, sel }; + return .{ .action = ._open_osc8, .selection = sel }; } - // Fall back to regex-based link detection - return self.linkAtPin(mouse_pin, mouse_mods); + // Fall back to configured links + return try self.linkAtPin(mouse_pin, mouse_mods); } -/// Core link detection at a pin position using regex patterns. -/// When mouse_mods is null, skips highlight/modifier checks (for double-click). +/// Detects if a link is present at the given pin. +/// +/// If mouse mods is null then mouse mod requirements are ignored (all +/// configured links are checked). /// /// Requires the renderer state mutex is held. fn linkAtPin( self: *Surface, mouse_pin: terminal.Pin, mouse_mods: ?input.Mods, -) !?struct { - input.Link.Action, - terminal.Selection, -} { - const screen: *terminal.Screen = self.renderer_state.terminal.screens.active; - +) !?Link { if (self.config.links.len == 0) return null; + const screen: *terminal.Screen = self.renderer_state.terminal.screens.active; const line = screen.selectLine(.{ .pin = mouse_pin, .whitespace = null, @@ -4409,12 +4415,10 @@ fn linkAtPin( for (self.config.links) |link| { // Skip highlight/mods check when mouse_mods is null (double-click mode) - if (mouse_mods) |mods| { - switch (link.highlight) { - .always, .hover => {}, - .always_mods, .hover_mods => |v| if (!v.equal(mods)) continue, - } - } + if (mouse_mods) |mods| switch (link.highlight) { + .always, .hover => {}, + .always_mods, .hover_mods => |v| if (!v.equal(mods)) continue, + }; var it = strmap.searchIterator(link.regex); while (true) { @@ -4422,7 +4426,10 @@ fn linkAtPin( defer match.deinit(); const sel = match.selection(); if (!sel.contains(screen, mouse_pin)) continue; - return .{ link.action, sel }; + return .{ + .action = link.action, + .selection = sel, + }; } } @@ -4453,11 +4460,11 @@ fn mouseModsWithCapture(self: *Surface, mods: input.Mods) input.Mods { /// /// Requires the renderer state mutex is held. fn processLinks(self: *Surface, pos: apprt.CursorPos) !bool { - const action, const sel = try self.linkAtPos(pos) orelse return false; - switch (action) { + const link = try self.linkAtPos(pos) orelse return false; + switch (link.action) { .open => { const str = try self.io.terminal.screens.active.selectionString(self.alloc, .{ - .sel = sel, + .sel = link.selection, .trim = false, }); defer self.alloc.free(str); @@ -4470,7 +4477,7 @@ fn processLinks(self: *Surface, pos: apprt.CursorPos) !bool { }, ._open_osc8 => { - const uri = self.osc8URI(sel.start()) orelse { + const uri = self.osc8URI(link.selection.start()) orelse { log.warn("failed to get URI for OSC8 hyperlink", .{}); return false; }; @@ -5313,11 +5320,11 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool self.renderer_state.mutex.lock(); defer self.renderer_state.mutex.unlock(); if (try self.linkAtPos(pos)) |link_info| { - const url_text = switch (link_info[0]) { + const url_text = switch (link_info.action) { .open => url_text: { // For regex links, get the text from selection break :url_text (self.io.terminal.screens.active.selectionString(self.alloc, .{ - .sel = link_info[1], + .sel = link_info.selection, .trim = self.config.clipboard_trim_trailing_spaces, })) catch |err| { log.err("error reading url string err={}", .{err}); @@ -5327,7 +5334,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool ._open_osc8 => url_text: { // For OSC8 links, get the URI directly from hyperlink data - const uri = self.osc8URI(link_info[1].start()) orelse { + const uri = self.osc8URI(link_info.selection.start()) orelse { log.warn("failed to get URI for OSC8 hyperlink", .{}); return false; };