mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 08:16:13 +03:00
renderer: matchSet matches OSC8
This commit is contained in:
@ -6,6 +6,7 @@ const inputpkg = @import("../input.zig");
|
||||
const terminal = @import("../terminal/main.zig");
|
||||
const point = terminal.point;
|
||||
const Screen = terminal.Screen;
|
||||
const Terminal = terminal.Terminal;
|
||||
|
||||
const log = std.log.scoped(.renderer_link);
|
||||
|
||||
@ -79,10 +80,125 @@ pub const Set = struct {
|
||||
var matches = std.ArrayList(terminal.Selection).init(alloc);
|
||||
defer matches.deinit();
|
||||
|
||||
// If our mouse is over an OSC8 link, then we can skip the regex
|
||||
// matches below since OSC8 takes priority.
|
||||
try self.matchSetFromOSC8(
|
||||
alloc,
|
||||
&matches,
|
||||
screen,
|
||||
mouse_pin,
|
||||
mouse_mods,
|
||||
);
|
||||
|
||||
// If we have no matches then we can try the regex matches.
|
||||
if (matches.items.len == 0) {
|
||||
try self.matchSetFromLinks(
|
||||
alloc,
|
||||
&matches,
|
||||
screen,
|
||||
mouse_pin,
|
||||
mouse_mods,
|
||||
);
|
||||
}
|
||||
|
||||
return .{ .matches = try matches.toOwnedSlice() };
|
||||
}
|
||||
|
||||
fn matchSetFromOSC8(
|
||||
self: *const Set,
|
||||
alloc: Allocator,
|
||||
matches: *std.ArrayList(terminal.Selection),
|
||||
screen: *Screen,
|
||||
mouse_pin: terminal.Pin,
|
||||
mouse_mods: inputpkg.Mods,
|
||||
) !void {
|
||||
_ = alloc;
|
||||
_ = self;
|
||||
|
||||
// If the right mods aren't pressed, then we can't match.
|
||||
if (!mouse_mods.equal(inputpkg.ctrlOrSuper(.{}))) return;
|
||||
|
||||
// Check if the cell the mouse is over is an OSC8 hyperlink
|
||||
const mouse_cell = mouse_pin.rowAndCell().cell;
|
||||
if (!mouse_cell.hyperlink) return;
|
||||
|
||||
// Get our hyperlink entry
|
||||
const page = &mouse_pin.page.data;
|
||||
const link_id = page.lookupHyperlink(mouse_cell) orelse {
|
||||
log.warn("failed to find hyperlink for cell", .{});
|
||||
return;
|
||||
};
|
||||
|
||||
// Go through every row and find matching hyperlinks for the given ID.
|
||||
// Note the link ID is not the same as the OSC8 ID parameter. But
|
||||
// we hash hyperlinks by their contents which should achieve the same
|
||||
// thing so we can use the ID as a key.
|
||||
var current: ?terminal.Selection = null;
|
||||
var row_it = screen.pages.getTopLeft(.viewport).rowIterator(.right_down, null);
|
||||
while (row_it.next()) |row_pin| {
|
||||
const row = row_pin.rowAndCell().row;
|
||||
|
||||
// If the row doesn't have any hyperlinks then we're done
|
||||
// building our matching selection.
|
||||
if (!row.hyperlink) {
|
||||
if (current) |sel| {
|
||||
try matches.append(sel);
|
||||
current = null;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// We have hyperlinks, look for our own matching hyperlink.
|
||||
for (row_pin.cells(.right), 0..) |*cell, x| {
|
||||
const match = match: {
|
||||
if (cell.hyperlink) {
|
||||
if (row_pin.page.data.lookupHyperlink(cell)) |cell_link_id| {
|
||||
break :match cell_link_id == link_id;
|
||||
}
|
||||
}
|
||||
break :match false;
|
||||
};
|
||||
|
||||
// If we have a match, extend our selection or start a new
|
||||
// selection.
|
||||
if (match) {
|
||||
const cell_pin = row_pin.right(x);
|
||||
if (current) |*sel| {
|
||||
sel.endPtr().* = cell_pin;
|
||||
} else {
|
||||
current = terminal.Selection.init(
|
||||
cell_pin,
|
||||
cell_pin,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// No match, if we have a current selection then complete it.
|
||||
if (current) |sel| {
|
||||
try matches.append(sel);
|
||||
current = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fills matches with the matches from regex link matches.
|
||||
fn matchSetFromLinks(
|
||||
self: *const Set,
|
||||
alloc: Allocator,
|
||||
matches: *std.ArrayList(terminal.Selection),
|
||||
screen: *Screen,
|
||||
mouse_pin: terminal.Pin,
|
||||
mouse_mods: inputpkg.Mods,
|
||||
) !void {
|
||||
// Iterate over all the visible lines.
|
||||
var lineIter = screen.lineIterator(screen.pages.pin(.{
|
||||
.viewport = .{},
|
||||
}) orelse return .{});
|
||||
}) orelse return);
|
||||
while (lineIter.next()) |line_sel| {
|
||||
const strmap: terminal.StringMap = strmap: {
|
||||
var strmap: terminal.StringMap = undefined;
|
||||
@ -141,8 +257,6 @@ pub const Set = struct {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return .{ .matches = try matches.toOwnedSlice() };
|
||||
}
|
||||
};
|
||||
|
||||
@ -391,3 +505,66 @@ test "matchset mods no match" {
|
||||
.y = 2,
|
||||
} }).?));
|
||||
}
|
||||
|
||||
test "matchset osc8" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
// Initialize our terminal
|
||||
var t = try Terminal.init(alloc, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(alloc);
|
||||
const s = &t.screen;
|
||||
|
||||
try t.printString("ABC");
|
||||
try t.screen.startHyperlink("http://example.com", null);
|
||||
try t.printString("123");
|
||||
t.screen.endHyperlink();
|
||||
|
||||
// Get a set
|
||||
var set = try Set.fromConfig(alloc, &.{});
|
||||
defer set.deinit(alloc);
|
||||
|
||||
// No matches over the non-link
|
||||
{
|
||||
var match = try set.matchSet(
|
||||
alloc,
|
||||
&t.screen,
|
||||
.{ .x = 2, .y = 0 },
|
||||
inputpkg.ctrlOrSuper(.{}),
|
||||
);
|
||||
defer match.deinit(alloc);
|
||||
try testing.expectEqual(@as(usize, 0), match.matches.len);
|
||||
}
|
||||
|
||||
// Match over link
|
||||
var match = try set.matchSet(
|
||||
alloc,
|
||||
&t.screen,
|
||||
.{ .x = 3, .y = 0 },
|
||||
inputpkg.ctrlOrSuper(.{}),
|
||||
);
|
||||
defer match.deinit(alloc);
|
||||
try testing.expectEqual(@as(usize, 1), match.matches.len);
|
||||
|
||||
// Test our matches
|
||||
try testing.expect(!match.orderedContains(s, s.pages.pin(.{ .screen = .{
|
||||
.x = 2,
|
||||
.y = 0,
|
||||
} }).?));
|
||||
try testing.expect(match.orderedContains(s, s.pages.pin(.{ .screen = .{
|
||||
.x = 3,
|
||||
.y = 0,
|
||||
} }).?));
|
||||
try testing.expect(match.orderedContains(s, s.pages.pin(.{ .screen = .{
|
||||
.x = 4,
|
||||
.y = 0,
|
||||
} }).?));
|
||||
try testing.expect(match.orderedContains(s, s.pages.pin(.{ .screen = .{
|
||||
.x = 5,
|
||||
.y = 0,
|
||||
} }).?));
|
||||
try testing.expect(!match.orderedContains(s, s.pages.pin(.{ .screen = .{
|
||||
.x = 6,
|
||||
.y = 0,
|
||||
} }).?));
|
||||
}
|
||||
|
@ -916,7 +916,7 @@ pub const Page = struct {
|
||||
}
|
||||
|
||||
/// Returns the hyperlink ID for the given cell.
|
||||
pub fn lookupHyperlink(self: *const Page, cell: *Cell) ?hyperlink.Id {
|
||||
pub fn lookupHyperlink(self: *const Page, cell: *const Cell) ?hyperlink.Id {
|
||||
const cell_offset = getOffset(Cell, self.memory, cell);
|
||||
const map = self.hyperlink_map.map(self.memory);
|
||||
return map.get(cell_offset);
|
||||
|
Reference in New Issue
Block a user