mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-14 03:25:50 +00:00
tweaks to link detection
This commit is contained in:
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user