tweaks to link detection

This commit is contained in:
Mitchell Hashimoto
2026-01-07 10:01:45 -08:00
parent 5a042570c8
commit 6659315760

View File

@@ -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;
};