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 |
| ------------------------------ | :---: | :------: | :-------: |
| RGB | ✅ | ✅ | ✅ |
| Hyperlinks | ✅ | planned | ❌ |
| Hyperlinks | ✅ | | ❌ |
| Bracketed Paste | ✅ | ✅ | ❌ |
| Kitty Keyboard | ✅ | ✅ | ✅ |
| Styled Underlines | ✅ | ✅ | ✅ |

View file

@ -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;
}

View file

@ -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,

View file

@ -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);
}