terminal: OSC8 hyperlinks in render state
parent
81142265aa
commit
fa26e9a384
|
|
@ -5,6 +5,7 @@ const wuffs = @import("wuffs");
|
|||
const apprt = @import("../apprt.zig");
|
||||
const configpkg = @import("../config.zig");
|
||||
const font = @import("../font/main.zig");
|
||||
const inputpkg = @import("../input.zig");
|
||||
const os = @import("../os/main.zig");
|
||||
const terminal = @import("../terminal/main.zig");
|
||||
const renderer = @import("../renderer.zig");
|
||||
|
|
@ -1068,6 +1069,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
|||
|
||||
// Data we extract out of the critical area.
|
||||
const Critical = struct {
|
||||
osc8_links: terminal.RenderState.CellSet,
|
||||
preedit: ?renderer.State.Preedit,
|
||||
cursor_style: ?renderer.CursorStyle,
|
||||
scrollbar: terminal.Scrollbar,
|
||||
|
|
@ -1131,7 +1133,27 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
|||
try self.prepKittyGraphics(state.terminal);
|
||||
}
|
||||
|
||||
// Get our OSC8 links we're hovering if we have a mouse.
|
||||
// This requires terminal state because of URLs.
|
||||
const osc8_links: terminal.RenderState.CellSet = osc8: {
|
||||
// If our mouse isn't hovering, we have no links.
|
||||
const vp = state.mouse.point orelse break :osc8 .empty;
|
||||
|
||||
// If the right mods aren't pressed, then we can't match.
|
||||
if (!state.mouse.mods.equal(inputpkg.ctrlOrSuper(.{})))
|
||||
break :osc8 .empty;
|
||||
|
||||
break :osc8 self.terminal_state.linkCells(
|
||||
arena_alloc,
|
||||
vp,
|
||||
) catch |err| {
|
||||
log.warn("error searching for OSC8 links err={}", .{err});
|
||||
break :osc8 .empty;
|
||||
};
|
||||
};
|
||||
|
||||
break :critical .{
|
||||
.osc8_links = osc8_links,
|
||||
.preedit = preedit,
|
||||
.cursor_style = cursor_style,
|
||||
.scrollbar = scrollbar,
|
||||
|
|
@ -1142,6 +1164,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
|||
try self.rebuildCells(
|
||||
critical.preedit,
|
||||
critical.cursor_style,
|
||||
&critical.osc8_links,
|
||||
);
|
||||
|
||||
// Notify our shaper we're done for the frame. For some shapers,
|
||||
|
|
@ -2225,6 +2248,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
|||
self: *Self,
|
||||
preedit: ?renderer.State.Preedit,
|
||||
cursor_style_: ?renderer.CursorStyle,
|
||||
osc8_links: *const terminal.RenderState.CellSet,
|
||||
) !void {
|
||||
const state: *terminal.RenderState = &self.terminal_state;
|
||||
defer state.redraw = false;
|
||||
|
|
@ -2619,18 +2643,23 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
|||
continue;
|
||||
}
|
||||
|
||||
// TODO: renderstate
|
||||
// Give links a single underline, unless they already have
|
||||
// an underline, in which case use a double underline to
|
||||
// distinguish them.
|
||||
// const underline: terminal.Attribute.Underline = if (link_match_set.contains(screen, cell_pin))
|
||||
// if (style.flags.underline == .single)
|
||||
// .double
|
||||
// else
|
||||
// .single
|
||||
// else
|
||||
// style.flags.underline;
|
||||
const underline = style.flags.underline;
|
||||
const underline: terminal.Attribute.Underline = underline: {
|
||||
// TODO: renderstate regex links
|
||||
|
||||
if (osc8_links.contains(.{
|
||||
.x = @intCast(x),
|
||||
.y = @intCast(y),
|
||||
})) {
|
||||
break :underline if (style.flags.underline == .single)
|
||||
.double
|
||||
else
|
||||
.single;
|
||||
}
|
||||
break :underline style.flags.underline;
|
||||
};
|
||||
|
||||
// We draw underlines first so that they layer underneath text.
|
||||
// This improves readability when a colored underline is used
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ const Terminal = @import("Terminal.zig");
|
|||
// - tests for cursor state
|
||||
// - tests for dirty state
|
||||
// - tests for colors
|
||||
// - tests for linkCells
|
||||
|
||||
// Developer note: this is in src/terminal and not src/renderer because
|
||||
// the goal is that this remains generic to multiple renderers. This can
|
||||
|
|
@ -514,6 +515,84 @@ pub const RenderState = struct {
|
|||
t.flags.dirty = .{};
|
||||
s.dirty = .{};
|
||||
}
|
||||
|
||||
/// A set of coordinates representing cells.
|
||||
pub const CellSet = std.AutoArrayHashMapUnmanaged(point.Coordinate, void);
|
||||
|
||||
/// Returns a map of the cells that match to an OSC8 hyperlink over the
|
||||
/// given point in the render state.
|
||||
///
|
||||
/// IMPORTANT: The terminal must not have updated since the last call to
|
||||
/// `update`. If there is any chance the terminal has updated, the caller
|
||||
/// must first call `update` again to refresh the render state.
|
||||
///
|
||||
/// For example, you may want to hold a lock for the duration of the
|
||||
/// update and hyperlink lookup to ensure no updates happen in between.
|
||||
pub fn linkCells(
|
||||
self: *const RenderState,
|
||||
alloc: Allocator,
|
||||
viewport_point: point.Coordinate,
|
||||
) Allocator.Error!CellSet {
|
||||
var result: CellSet = .empty;
|
||||
errdefer result.deinit(alloc);
|
||||
|
||||
const row_slice = self.row_data.slice();
|
||||
const row_pins = row_slice.items(.pin);
|
||||
const row_cells = row_slice.items(.cells);
|
||||
|
||||
// Grab our link ID
|
||||
const link_page: *page.Page = &row_pins[viewport_point.y].node.data;
|
||||
const link = link: {
|
||||
const rac = link_page.getRowAndCell(
|
||||
viewport_point.x,
|
||||
viewport_point.y,
|
||||
);
|
||||
|
||||
// The likely scenario is that our mouse isn't even over a link.
|
||||
if (!rac.cell.hyperlink) {
|
||||
@branchHint(.likely);
|
||||
return result;
|
||||
}
|
||||
|
||||
const link_id = link_page.lookupHyperlink(rac.cell) orelse
|
||||
return result;
|
||||
break :link link_page.hyperlink_set.get(
|
||||
link_page.memory,
|
||||
link_id,
|
||||
);
|
||||
};
|
||||
|
||||
for (
|
||||
0..,
|
||||
row_pins,
|
||||
row_cells,
|
||||
) |y, pin, cells| {
|
||||
for (0.., cells.items(.raw)) |x, cell| {
|
||||
if (!cell.hyperlink) continue;
|
||||
|
||||
const other_page: *page.Page = &pin.node.data;
|
||||
const other = link: {
|
||||
const rac = other_page.getRowAndCell(x, y);
|
||||
const link_id = other_page.lookupHyperlink(rac.cell) orelse continue;
|
||||
break :link other_page.hyperlink_set.get(
|
||||
other_page.memory,
|
||||
link_id,
|
||||
);
|
||||
};
|
||||
|
||||
if (link.eql(
|
||||
link_page.memory,
|
||||
other,
|
||||
other_page.memory,
|
||||
)) try result.put(alloc, .{
|
||||
.y = @intCast(y),
|
||||
.x = @intCast(x),
|
||||
}, {});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
test "styled" {
|
||||
|
|
|
|||
Loading…
Reference in New Issue