vaxis: implement hyperlinks (osc8)

This requires additional allocations anytime there is a hyperlink

Signed-off-by: Tim Culverhouse <tim@timculverhouse.com>
This commit is contained in:
Tim Culverhouse 2024-01-24 19:04:12 -06:00
parent 9950c9eac5
commit 8f311da873
4 changed files with 41 additions and 16 deletions

View file

@ -18,7 +18,7 @@ Contributions are welcome.
| Feature | Vaxis | libvaxis | notcurses | | Feature | Vaxis | libvaxis | notcurses |
| ------------------------------ | :---: | :------: | :-------: | | ------------------------------ | :---: | :------: | :-------: |
| RGB | ✅ | ✅ | ✅ | | RGB | ✅ | ✅ | ✅ |
| Hyperlinks | ✅ | planned | ❌ | | Hyperlinks | ✅ | | ❌ |
| Bracketed Paste | ✅ | ✅ | ❌ | | Bracketed Paste | ✅ | ✅ | ❌ |
| Kitty Keyboard | ✅ | ✅ | ✅ | | Kitty Keyboard | ✅ | ✅ | ✅ |
| Styled Underlines | ✅ | ✅ | ✅ | | Styled Underlines | ✅ | ✅ | ✅ |

View file

@ -11,11 +11,16 @@ const InternalScreen = @This();
pub const InternalCell = struct { pub const InternalCell = struct {
char: std.ArrayList(u8) = undefined, char: std.ArrayList(u8) = undefined,
style: Style = .{}, style: Style = .{},
uri: std.ArrayList(u8) = undefined,
uri_id: std.ArrayList(u8) = undefined,
// if we got skipped because of a wide character // if we got skipped because of a wide character
skipped: bool = false, skipped: bool = false,
pub fn eql(self: InternalCell, cell: Cell) bool { 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| { for (screen.buf, 0..) |_, i| {
screen.buf[i] = .{ screen.buf[i] = .{
.char = try std.ArrayList(u8).initCapacity(alloc, 1), .char = try std.ArrayList(u8).initCapacity(alloc, 1),
.uri = std.ArrayList(u8).init(alloc),
.uri_id = std.ArrayList(u8).init(alloc),
}; };
} }
screen.width = w; 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 { pub fn deinit(self: *InternalScreen, alloc: std.mem.Allocator) void {
for (self.buf, 0..) |_, i| { for (self.buf, 0..) |_, i| {
self.buf[i].char.deinit(); self.buf[i].char.deinit();
self.buf[i].uri.deinit();
self.buf[i].uri_id.deinit();
} }
alloc.free(self.buf); alloc.free(self.buf);
@ -57,8 +66,7 @@ pub fn writeCell(
self: *InternalScreen, self: *InternalScreen,
col: usize, col: usize,
row: usize, row: usize,
char: []const u8, cell: Cell,
style: Style,
) void { ) void {
if (self.width < col) { if (self.width < col) {
// column out of bounds // column out of bounds
@ -71,8 +79,16 @@ pub fn writeCell(
const i = (row * self.width) + col; const i = (row * self.width) + col;
assert(i < self.buf.len); assert(i < self.buf.len);
self.buf[i].char.clearRetainingCapacity(); 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", .{}); 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;
} }

View file

@ -1,6 +1,7 @@
pub const Cell = struct { pub const Cell = struct {
char: Character = .{}, char: Character = .{},
style: Style = .{}, style: Style = .{},
link: Hyperlink = .{},
}; };
pub const Character = struct { pub const Character = struct {
@ -8,6 +9,12 @@ pub const Character = struct {
width: usize = 1, width: usize = 1,
}; };
pub const Hyperlink = struct {
uri: []const u8 = "",
/// ie "id=app-1234"
params: []const u8 = "",
};
pub const Style = struct { pub const Style = struct {
pub const Underline = enum { pub const Underline = enum {
off, off,
@ -24,8 +31,6 @@ pub const Style = struct {
ul_style: Underline = .off, ul_style: Underline = .off,
// TODO: url should maybe go outside of style. We'll need to allocate these // TODO: url should maybe go outside of style. We'll need to allocate these
// in the internal screen // in the internal screen
url: ?[]const u8 = null,
url_params: ?[]const u8 = null,
bold: bool = false, bold: bool = false,
dim: bool = false, dim: bool = false,

View file

@ -11,6 +11,7 @@ const InternalScreen = @import("InternalScreen.zig");
const Window = @import("Window.zig"); const Window = @import("Window.zig");
const Options = @import("Options.zig"); const Options = @import("Options.zig");
const Style = @import("cell.zig").Style; const Style = @import("cell.zig").Style;
const Hyperlink = @import("cell.zig").Hyperlink;
const gwidth = @import("gwidth.zig"); const gwidth = @import("gwidth.zig");
const Shape = @import("Mouse.zig").Shape; const Shape = @import("Mouse.zig").Shape;
@ -275,6 +276,7 @@ pub fn Vaxis(comptime T: type) type {
var row: usize = 0; var row: usize = 0;
var col: usize = 0; var col: usize = 0;
var cursor: Style = .{}; var cursor: Style = .{};
var link: Hyperlink = .{};
var i: usize = 0; var i: usize = 0;
while (i < self.screen.buf.len) { while (i < self.screen.buf.len) {
@ -301,15 +303,18 @@ pub fn Vaxis(comptime T: type) type {
reposition = true; reposition = true;
// Close any osc8 sequence we might be in before // Close any osc8 sequence we might be in before
// repositioning // repositioning
if (cursor.url) |_| { if (link.uri.len > 0) {
_ = try tty.write(ctlseqs.osc8_clear); _ = try tty.write(ctlseqs.osc8_clear);
} }
continue; continue;
} }
self.screen_last.buf[i].skipped = false; 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 // 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 // reposition the cursor, if needed
if (reposition) { if (reposition) {
@ -442,16 +447,15 @@ pub fn Vaxis(comptime T: type) type {
} }
// url // url
if (!std.meta.eql(cursor.url, cell.style.url)) { if (!std.meta.eql(link.uri, cell.link.uri)) {
const url = cell.style.url orelse ""; var ps = cell.link.params;
var ps = cell.style.url_params orelse ""; if (cell.link.uri.len == 0) {
if (url.len == 0) {
// Empty out the params no matter what if we don't have // Empty out the params no matter what if we don't have
// a url // a url
ps = ""; ps = "";
} }
const writer = tty.buffered_writer.writer(); 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); _ = try tty.write(cell.char.grapheme);
} }