From a3a445a0666338a63c29cfcf1717db38f1653f26 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 3 Jul 2024 21:17:12 -0700 Subject: [PATCH] terminal: print sets hyperlink state, tests --- src/terminal/Terminal.zig | 111 +++++++++++++++++++++++++++++++++++++- src/terminal/page.zig | 54 ++++++++++++++++++- 2 files changed, 163 insertions(+), 2 deletions(-) diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 8dcb46133..4b9899da9 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -14,6 +14,7 @@ const ansi = @import("ansi.zig"); const modes = @import("modes.zig"); const charsets = @import("charsets.zig"); const csi = @import("csi.zig"); +const hyperlink = @import("hyperlink.zig"); const kitty = @import("kitty.zig"); const point = @import("point.zig"); const sgr = @import("sgr.zig"); @@ -600,10 +601,26 @@ fn printCell( ); } + // We check for an active hyperlink first because setHyperlink + // handles clearing the old hyperlink and an optimization if we're + // overwriting the same hyperlink. + if (self.screen.cursor.hyperlink_id > 0) { + // If we have a hyperlink configured, apply it to this cell + var page = &self.screen.cursor.page_pin.page.data; + page.setHyperlink(cell, self.screen.cursor.hyperlink_id) catch |err| { + // TODO: an error can only happen if our page is out of space + // so realloc the page here. + log.err("failed to set hyperlink, ignoring err={}", .{err}); + }; + } else if (cell.hyperlink) { + // If the previous cell had a hyperlink then we need to clear it. + var page = &self.screen.cursor.page_pin.page.data; + page.clearHyperlink(cell); + } + // We don't need to update the style refs unless the // cell's new style will be different after writing. const style_changed = cell.style_id != self.screen.cursor.style_id; - if (style_changed) { var page = &self.screen.cursor.page_pin.page.data; @@ -621,6 +638,7 @@ fn printCell( .style_id = self.screen.cursor.style_id, .wide = wide, .protected = self.screen.cursor.protected, + .hyperlink = self.screen.cursor.hyperlink_id > 0, }; if (style_changed) { @@ -3775,6 +3793,97 @@ test "Terminal: print wide char at right margin does not create spacer head" { } } +test "Terminal: print with hyperlink" { + var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); + defer t.deinit(testing.allocator); + + // Setup our hyperlink and print + try t.screen.startHyperlink("http://example.com", null); + try t.printString("123456"); + + // Verify all our cells have a hyperlink + for (0..6) |x| { + const list_cell = t.screen.pages.getCell(.{ .screen = .{ + .x = @intCast(x), + .y = 0, + } }).?; + const cell = list_cell.cell; + try testing.expect(cell.hyperlink); + const id = list_cell.page.data.lookupHyperlink(cell).?; + try testing.expectEqual(@as(hyperlink.Id, 1), id); + } + + try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); +} + +test "Terminal: print and end hyperlink" { + var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); + defer t.deinit(testing.allocator); + + // Setup our hyperlink and print + try t.screen.startHyperlink("http://example.com", null); + try t.printString("123"); + t.screen.endHyperlink(); + try t.printString("456"); + + // Verify all our cells have a hyperlink + for (0..3) |x| { + const list_cell = t.screen.pages.getCell(.{ .screen = .{ + .x = @intCast(x), + .y = 0, + } }).?; + const cell = list_cell.cell; + try testing.expect(cell.hyperlink); + const id = list_cell.page.data.lookupHyperlink(cell).?; + try testing.expectEqual(@as(hyperlink.Id, 1), id); + } + for (3..6) |x| { + const list_cell = t.screen.pages.getCell(.{ .screen = .{ + .x = @intCast(x), + .y = 0, + } }).?; + const cell = list_cell.cell; + try testing.expect(!cell.hyperlink); + } + + try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); +} + +test "Terminal: print and change hyperlink" { + var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); + defer t.deinit(testing.allocator); + + // Setup our hyperlink and print + try t.screen.startHyperlink("http://one.example.com", null); + try t.printString("123"); + try t.screen.startHyperlink("http://two.example.com", null); + try t.printString("456"); + + // Verify all our cells have a hyperlink + for (0..3) |x| { + const list_cell = t.screen.pages.getCell(.{ .screen = .{ + .x = @intCast(x), + .y = 0, + } }).?; + const cell = list_cell.cell; + try testing.expect(cell.hyperlink); + const id = list_cell.page.data.lookupHyperlink(cell).?; + try testing.expectEqual(@as(hyperlink.Id, 1), id); + } + for (3..6) |x| { + const list_cell = t.screen.pages.getCell(.{ .screen = .{ + .x = @intCast(x), + .y = 0, + } }).?; + const cell = list_cell.cell; + try testing.expect(cell.hyperlink); + const id = list_cell.page.data.lookupHyperlink(cell).?; + try testing.expectEqual(@as(hyperlink.Id, 2), id); + } + + try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); +} + test "Terminal: linefeed and carriage return" { var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); defer t.deinit(testing.allocator); diff --git a/src/terminal/page.zig b/src/terminal/page.zig index da3b0d2f5..86d3fefb4 100644 --- a/src/terminal/page.zig +++ b/src/terminal/page.zig @@ -859,6 +859,53 @@ pub const Page = struct { @memset(@as([]u64, @ptrCast(cells)), 0); } + /// Returns the hyperlink ID for the given cell. + pub fn lookupHyperlink(self: *const Page, cell: *Cell) ?hyperlink.Id { + const cell_offset = getOffset(Cell, self.memory, cell); + const map = self.hyperlink_map.map(self.memory); + return map.get(cell_offset); + } + + /// Clear the hyperlink from the given cell. + pub fn clearHyperlink(self: *Page, cell: *Cell) void { + defer self.assertIntegrity(); + + // Get our ID + const cell_offset = getOffset(Cell, self.memory, cell); + var map = self.hyperlink_map.map(self.memory); + const entry = map.getEntry(cell_offset) orelse return; + + // Release our usage of this + self.hyperlink_set.release(self.memory, entry.value_ptr.*); + + // Free the memory + map.removeByPtr(entry.key_ptr); + } + + /// Set the hyperlink for the given cell. If the cell already has a + /// hyperlink, then this will handle memory management for the prior + /// hyperlink. + pub fn setHyperlink(self: *Page, cell: *Cell, id: hyperlink.Id) !void { + defer self.assertIntegrity(); + + const cell_offset = getOffset(Cell, self.memory, cell); + var map = self.hyperlink_map.map(self.memory); + const gop = try map.getOrPut(cell_offset); + + if (gop.found_existing) { + // If the hyperlink matches then we don't need to do anything. + if (gop.value_ptr.* == id) return; + + // Different hyperlink, we need to release the old one + self.hyperlink_set.release(self.memory, gop.value_ptr.*); + } + + // Increase ref count for our new hyperlink and set it + self.hyperlink_set.use(self.memory, id); + gop.value_ptr.* = id; + cell.hyperlink = true; + } + /// Append a codepoint to the given cell as a grapheme. pub fn appendGrapheme(self: *Page, row: *Row, cell: *Cell, cp: u21) Allocator.Error!void { defer self.assertIntegrity(); @@ -1297,7 +1344,12 @@ pub const Cell = packed struct(u64) { /// Whether this was written with the protection flag set. protected: bool = false, - _padding: u19 = 0, + /// Whether this cell is a hyperlink. If this is true then you must + /// look up the hyperlink ID in the page hyperlink_map and the ID in + /// the hyperlink_set to get the actual hyperlink data. + hyperlink: bool = false, + + _padding: u18 = 0, pub const ContentTag = enum(u2) { /// A single codepoint, could be zero to be empty cell.