From 58b6ae9655fe8334d39ead25630292b1cf3d2214 Mon Sep 17 00:00:00 2001 From: Mustaque Ahmed Date: Sun, 16 Feb 2025 18:33:21 +0530 Subject: [PATCH] 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 . 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. --- src/Surface.zig | 71 +++++++++++++++++++++++++++++++++++++++------- src/config/url.zig | 14 +-------- 2 files changed, 62 insertions(+), 23 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 70c32098f..06701b536 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1053,10 +1053,10 @@ fn mouseRefreshLinks( .pointer, ); - switch (link[0]) { + switch (link.action) { .open => { const str = try self.io.terminal.screen.selectionString(self.alloc, .{ - .sel = link[1], + .sel = link.selection, .trim = false, }); defer self.alloc.free(str); @@ -1069,7 +1069,7 @@ fn mouseRefreshLinks( ._open_osc8 => link: { // 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; @@ -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. /// /// Requires the renderer mutex is held. fn linkAtPos( self: *Surface, pos: apprt.CursorPos, -) !?struct { - input.Link.Action, - terminal.Selection, -} { +) !?ActionSelection { // Convert our cursor position to a screen point. const screen = &self.renderer_state.terminal.screen; const mouse_pin: terminal.Pin = mouse_pin: { @@ -3179,7 +3181,7 @@ 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 }; } // If we have no OSC8 links then we fallback to regex-based URL detection. @@ -3214,13 +3216,60 @@ fn linkAtPos( defer match.deinit(); const sel = match.selection(); 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; } +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 /// other purposes taking into account when shift is pressed for releasing /// the mouse from capture. @@ -3245,7 +3294,9 @@ 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; + const link = try self.linkAtPos(pos) orelse return false; + const action = link.action; + const sel = link.selection; switch (action) { .open => { const str = try self.io.terminal.screen.selectionString(self.alloc, .{ diff --git a/src/config/url.zig b/src/config/url.zig index 78f9816fd..dcc1902e6 100644 --- a/src/config/url.zig +++ b/src/config/url.zig @@ -26,7 +26,7 @@ pub const regex = "(?:" ++ url_schemes ++ \\)(?: ++ ipv6_url_pattern ++ - \\|[\w\-.~:/?#@!$&*+,;=%]+(?:[\(\[]\w*[\)\]])?)+(?