From 8f311da873abbb4c0d8cc10a515b87b7add832e0 Mon Sep 17 00:00:00 2001 From: Tim Culverhouse Date: Wed, 24 Jan 2024 19:04:12 -0600 Subject: [PATCH] vaxis: implement hyperlinks (osc8) This requires additional allocations anytime there is a hyperlink Signed-off-by: Tim Culverhouse --- README.md | 2 +- src/InternalScreen.zig | 26 +++++++++++++++++++++----- src/cell.zig | 9 +++++++-- src/vaxis.zig | 20 ++++++++++++-------- 4 files changed, 41 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index b3e2f79..4ce26c7 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Contributions are welcome. | Feature | Vaxis | libvaxis | notcurses | | ------------------------------ | :---: | :------: | :-------: | | RGB | ✅ | ✅ | ✅ | -| Hyperlinks | ✅ | planned | ❌ | +| Hyperlinks | ✅ | ✅ | ❌ | | Bracketed Paste | ✅ | ✅ | ❌ | | Kitty Keyboard | ✅ | ✅ | ✅ | | Styled Underlines | ✅ | ✅ | ✅ | diff --git a/src/InternalScreen.zig b/src/InternalScreen.zig index c75e770..7bdda2d 100644 --- a/src/InternalScreen.zig +++ b/src/InternalScreen.zig @@ -11,11 +11,16 @@ const InternalScreen = @This(); pub const InternalCell = struct { char: std.ArrayList(u8) = undefined, style: Style = .{}, + uri: std.ArrayList(u8) = undefined, + uri_id: std.ArrayList(u8) = undefined, // if we got skipped because of a wide character skipped: bool = false, pub fn eql(self: InternalCell, cell: Cell) bool { - return std.mem.eql(u8, self.char.items, cell.char.grapheme) and std.meta.eql(self.style, cell.style); + return std.mem.eql(u8, self.char.items, cell.char.grapheme) and + std.meta.eql(self.style, cell.style) and + std.mem.eql(u8, self.uri.items, cell.link.uri) and + std.mem.eql(u8, self.uri_id.items, cell.link.params); } }; @@ -37,6 +42,8 @@ pub fn init(alloc: std.mem.Allocator, w: usize, h: usize) !InternalScreen { for (screen.buf, 0..) |_, i| { screen.buf[i] = .{ .char = try std.ArrayList(u8).initCapacity(alloc, 1), + .uri = std.ArrayList(u8).init(alloc), + .uri_id = std.ArrayList(u8).init(alloc), }; } screen.width = w; @@ -47,6 +54,8 @@ pub fn init(alloc: std.mem.Allocator, w: usize, h: usize) !InternalScreen { pub fn deinit(self: *InternalScreen, alloc: std.mem.Allocator) void { for (self.buf, 0..) |_, i| { self.buf[i].char.deinit(); + self.buf[i].uri.deinit(); + self.buf[i].uri_id.deinit(); } alloc.free(self.buf); @@ -57,8 +66,7 @@ pub fn writeCell( self: *InternalScreen, col: usize, row: usize, - char: []const u8, - style: Style, + cell: Cell, ) void { if (self.width < col) { // column out of bounds @@ -71,8 +79,16 @@ pub fn writeCell( const i = (row * self.width) + col; assert(i < self.buf.len); self.buf[i].char.clearRetainingCapacity(); - self.buf[i].char.appendSlice(char) catch { + self.buf[i].char.appendSlice(cell.char.grapheme) catch { log.warn("couldn't write grapheme", .{}); }; - self.buf[i].style = style; + self.buf[i].uri.clearRetainingCapacity(); + self.buf[i].uri.appendSlice(cell.link.uri) catch { + log.warn("couldn't write uri", .{}); + }; + self.buf[i].uri.clearRetainingCapacity(); + self.buf[i].uri_id.appendSlice(cell.link.params) catch { + log.warn("couldn't write uri_id", .{}); + }; + self.buf[i].style = cell.style; } diff --git a/src/cell.zig b/src/cell.zig index c8c7b8e..08260c1 100644 --- a/src/cell.zig +++ b/src/cell.zig @@ -1,6 +1,7 @@ pub const Cell = struct { char: Character = .{}, style: Style = .{}, + link: Hyperlink = .{}, }; pub const Character = struct { @@ -8,6 +9,12 @@ pub const Character = struct { width: usize = 1, }; +pub const Hyperlink = struct { + uri: []const u8 = "", + /// ie "id=app-1234" + params: []const u8 = "", +}; + pub const Style = struct { pub const Underline = enum { off, @@ -24,8 +31,6 @@ pub const Style = struct { ul_style: Underline = .off, // TODO: url should maybe go outside of style. We'll need to allocate these // in the internal screen - url: ?[]const u8 = null, - url_params: ?[]const u8 = null, bold: bool = false, dim: bool = false, diff --git a/src/vaxis.zig b/src/vaxis.zig index a1c756a..01fc846 100644 --- a/src/vaxis.zig +++ b/src/vaxis.zig @@ -11,6 +11,7 @@ const InternalScreen = @import("InternalScreen.zig"); const Window = @import("Window.zig"); const Options = @import("Options.zig"); const Style = @import("cell.zig").Style; +const Hyperlink = @import("cell.zig").Hyperlink; const gwidth = @import("gwidth.zig"); const Shape = @import("Mouse.zig").Shape; @@ -275,6 +276,7 @@ pub fn Vaxis(comptime T: type) type { var row: usize = 0; var col: usize = 0; var cursor: Style = .{}; + var link: Hyperlink = .{}; var i: usize = 0; while (i < self.screen.buf.len) { @@ -301,15 +303,18 @@ pub fn Vaxis(comptime T: type) type { reposition = true; // Close any osc8 sequence we might be in before // repositioning - if (cursor.url) |_| { + if (link.uri.len > 0) { _ = try tty.write(ctlseqs.osc8_clear); } continue; } self.screen_last.buf[i].skipped = false; - defer cursor = cell.style; + defer { + cursor = cell.style; + link = cell.link; + } // Set this cell in the last frame - self.screen_last.writeCell(col, row, cell.char.grapheme, cell.style); + self.screen_last.writeCell(col, row, cell); // reposition the cursor, if needed if (reposition) { @@ -442,16 +447,15 @@ pub fn Vaxis(comptime T: type) type { } // url - if (!std.meta.eql(cursor.url, cell.style.url)) { - const url = cell.style.url orelse ""; - var ps = cell.style.url_params orelse ""; - if (url.len == 0) { + if (!std.meta.eql(link.uri, cell.link.uri)) { + var ps = cell.link.params; + if (cell.link.uri.len == 0) { // Empty out the params no matter what if we don't have // a url ps = ""; } const writer = tty.buffered_writer.writer(); - try std.fmt.format(writer, ctlseqs.osc8, .{ ps, url }); + try std.fmt.format(writer, ctlseqs.osc8, .{ ps, cell.link.uri }); } _ = try tty.write(cell.char.grapheme); }