feat: add support for relative paths

to make relative path clickable we'll check whether we're able to
resolve path in local system or not? We check line and it's all
substring seperated by <space>. If path is resolvable then we will build
the selection and return the result i.e. `ActionSelection` struct. `ActionSelection`
is contains same `action` and `selection` properties.
This commit is contained in:
Mustaque Ahmed
2025-02-16 18:33:21 +05:30
parent 29c2f095a6
commit 58b6ae9655
2 changed files with 62 additions and 23 deletions

View File

@ -1053,10 +1053,10 @@ fn mouseRefreshLinks(
.pointer, .pointer,
); );
switch (link[0]) { switch (link.action) {
.open => { .open => {
const str = try self.io.terminal.screen.selectionString(self.alloc, .{ const str = try self.io.terminal.screen.selectionString(self.alloc, .{
.sel = link[1], .sel = link.selection,
.trim = false, .trim = false,
}); });
defer self.alloc.free(str); defer self.alloc.free(str);
@ -1069,7 +1069,7 @@ fn mouseRefreshLinks(
._open_osc8 => link: { ._open_osc8 => link: {
// Show the URL in the status bar // Show the URL in the status bar
const pin = link[1].start(); const pin = link.selection.start();
const uri = self.osc8URI(pin) orelse { const uri = self.osc8URI(pin) orelse {
log.warn("failed to get URI for OSC8 hyperlink", .{}); log.warn("failed to get URI for OSC8 hyperlink", .{});
break :link; break :link;
@ -3149,16 +3149,18 @@ fn clickMoveCursor(self: *Surface, to: terminal.Pin) !void {
} }
} }
const ActionSelection = struct {
action: input.Link.Action,
selection: terminal.Selection,
};
/// Returns the link at the given cursor position, if any. /// Returns the link at the given cursor position, if any.
/// ///
/// Requires the renderer mutex is held. /// Requires the renderer mutex is held.
fn linkAtPos( fn linkAtPos(
self: *Surface, self: *Surface,
pos: apprt.CursorPos, pos: apprt.CursorPos,
) !?struct { ) !?ActionSelection {
input.Link.Action,
terminal.Selection,
} {
// Convert our cursor position to a screen point. // Convert our cursor position to a screen point.
const screen = &self.renderer_state.terminal.screen; const screen = &self.renderer_state.terminal.screen;
const mouse_pin: terminal.Pin = mouse_pin: { const mouse_pin: terminal.Pin = mouse_pin: {
@ -3179,7 +3181,7 @@ fn linkAtPos(
const cell = rac.cell; const cell = rac.cell;
if (!cell.hyperlink) break :hyperlink; if (!cell.hyperlink) break :hyperlink;
const sel = terminal.Selection.init(mouse_pin, mouse_pin, false); const sel = terminal.Selection.init(mouse_pin, mouse_pin, false);
return .{ ._open_osc8, sel }; return .{ .action = ._open_osc8, .selection = sel };
} }
// If we have no OSC8 links then we fallback to regex-based URL detection. // If we have no OSC8 links then we fallback to regex-based URL detection.
@ -3214,13 +3216,60 @@ fn linkAtPos(
defer match.deinit(); defer match.deinit();
const sel = match.selection(); const sel = match.selection();
if (!sel.contains(screen, mouse_pin)) continue; if (!sel.contains(screen, mouse_pin)) continue;
return .{ link.action, sel }; return .{ .action = link.action, .selection = sel };
}
}
// this is the last chance to return any substr as clickable or not
// by checking if any given str is path resolvable or not. below lines of code
// will work only for file paths. this covers both relative and absolute paths.
// some of the paths may not work it is based on the `std.fs.realpath`
const cwd = self.io.terminal.getPwd();
var split_str = std.mem.splitSequence(u8, strmap.string, " ");
while (split_str.next()) |potential_path| {
const path_slice: []const []const u8 = &[_][]const u8{
cwd.?,
potential_path,
};
const cwd_path = try std.fs.path.join(self.alloc, path_slice);
defer self.alloc.free(cwd_path);
// we can skip opening the current path as we're already at the right path
if (std.mem.eql(u8, cwd.?, cwd_path)) {
continue;
}
var buf: [std.fs.max_path_bytes]u8 = undefined;
const real_path = std.fs.realpath(cwd_path, &buf) catch null;
if (real_path != null) {
const region = findSubstringIndices(strmap.string, potential_path);
if (region) |r| {
const start_idx = r[0];
const end_idx = r[1];
const start_pt = strmap.map[start_idx];
const end_pt = strmap.map[end_idx + 1];
const sel = screen.selectWordBetween(start_pt, end_pt);
if (sel) |s| {
const result = .{
.action = input.Link.Action{ .open = {} },
.selection = s,
};
return result;
}
}
} }
} }
return null; return null;
} }
fn findSubstringIndices(haystack: []const u8, needle: []const u8) ?[2]usize {
const start_index = std.mem.indexOf(u8, haystack, needle) orelse return null;
const end_index = start_index + needle.len;
return .{ start_index, end_index };
}
/// This returns the mouse mods to consider for link highlighting or /// This returns the mouse mods to consider for link highlighting or
/// other purposes taking into account when shift is pressed for releasing /// other purposes taking into account when shift is pressed for releasing
/// the mouse from capture. /// the mouse from capture.
@ -3245,7 +3294,9 @@ fn mouseModsWithCapture(self: *Surface, mods: input.Mods) input.Mods {
/// ///
/// Requires the renderer state mutex is held. /// Requires the renderer state mutex is held.
fn processLinks(self: *Surface, pos: apprt.CursorPos) !bool { fn processLinks(self: *Surface, pos: apprt.CursorPos) !bool {
const action, const sel = try self.linkAtPos(pos) orelse return false; const link = try self.linkAtPos(pos) orelse return false;
const action = link.action;
const sel = link.selection;
switch (action) { switch (action) {
.open => { .open => {
const str = try self.io.terminal.screen.selectionString(self.alloc, .{ const str = try self.io.terminal.screen.selectionString(self.alloc, .{

View File

@ -26,7 +26,7 @@ pub const regex =
"(?:" ++ url_schemes ++ "(?:" ++ url_schemes ++
\\)(?: \\)(?:
++ ipv6_url_pattern ++ ++ ipv6_url_pattern ++
\\|[\w\-.~:/?#@!$&*+,;=%]+(?:[\(\[]\w*[\)\]])?)+(?<![,.])|(?:\.\.\/|\.\/*|\/)[\w\-.~:\/?#@!$&*+,;=%]+(?:\/[\w\-.~:\/?#@!$&*+,;=%]*)* \\|[\w\-.~:/?#@!$&*+,;=%]+(?:[\(\[]\w*[\)\]])?)+(?<![,.])|(?:^\/)[\w\-.~:\/?#@!$&*+,;=%]+(?:\/[\w\-.~:\/?#@!$&*+,;=%]*)*
; ;
const url_schemes = const url_schemes =
\\https?://|mailto:|ftp://|file:|ssh:|git://|ssh://|tel:|magnet:|ipfs://|ipns://|gemini://|gopher://|news: \\https?://|mailto:|ftp://|file:|ssh:|git://|ssh://|tel:|magnet:|ipfs://|ipns://|gemini://|gopher://|news:
@ -184,18 +184,6 @@ test "url regex" {
.input = "/Users/ghostty.user/code/../example.py hello world", .input = "/Users/ghostty.user/code/../example.py hello world",
.expect = "/Users/ghostty.user/code/../example.py", .expect = "/Users/ghostty.user/code/../example.py",
}, },
.{
.input = "../example.py",
.expect = "../example.py",
},
.{
.input = "../example.py ",
.expect = "../example.py",
},
.{
.input = "first time ../example.py contributor ",
.expect = "../example.py",
},
.{ .{
.input = "[link](/home/user/ghostty.user/example)", .input = "[link](/home/user/ghostty.user/example)",
.expect = "/home/user/ghostty.user/example", .expect = "/home/user/ghostty.user/example",