mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +03:00
terminal: print sets hyperlink state, tests
This commit is contained in:
@ -14,6 +14,7 @@ const ansi = @import("ansi.zig");
|
|||||||
const modes = @import("modes.zig");
|
const modes = @import("modes.zig");
|
||||||
const charsets = @import("charsets.zig");
|
const charsets = @import("charsets.zig");
|
||||||
const csi = @import("csi.zig");
|
const csi = @import("csi.zig");
|
||||||
|
const hyperlink = @import("hyperlink.zig");
|
||||||
const kitty = @import("kitty.zig");
|
const kitty = @import("kitty.zig");
|
||||||
const point = @import("point.zig");
|
const point = @import("point.zig");
|
||||||
const sgr = @import("sgr.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
|
// We don't need to update the style refs unless the
|
||||||
// cell's new style will be different after writing.
|
// cell's new style will be different after writing.
|
||||||
const style_changed = cell.style_id != self.screen.cursor.style_id;
|
const style_changed = cell.style_id != self.screen.cursor.style_id;
|
||||||
|
|
||||||
if (style_changed) {
|
if (style_changed) {
|
||||||
var page = &self.screen.cursor.page_pin.page.data;
|
var page = &self.screen.cursor.page_pin.page.data;
|
||||||
|
|
||||||
@ -621,6 +638,7 @@ fn printCell(
|
|||||||
.style_id = self.screen.cursor.style_id,
|
.style_id = self.screen.cursor.style_id,
|
||||||
.wide = wide,
|
.wide = wide,
|
||||||
.protected = self.screen.cursor.protected,
|
.protected = self.screen.cursor.protected,
|
||||||
|
.hyperlink = self.screen.cursor.hyperlink_id > 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (style_changed) {
|
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" {
|
test "Terminal: linefeed and carriage return" {
|
||||||
var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
|
var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
|
||||||
defer t.deinit(testing.allocator);
|
defer t.deinit(testing.allocator);
|
||||||
|
@ -859,6 +859,53 @@ pub const Page = struct {
|
|||||||
@memset(@as([]u64, @ptrCast(cells)), 0);
|
@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.
|
/// Append a codepoint to the given cell as a grapheme.
|
||||||
pub fn appendGrapheme(self: *Page, row: *Row, cell: *Cell, cp: u21) Allocator.Error!void {
|
pub fn appendGrapheme(self: *Page, row: *Row, cell: *Cell, cp: u21) Allocator.Error!void {
|
||||||
defer self.assertIntegrity();
|
defer self.assertIntegrity();
|
||||||
@ -1297,7 +1344,12 @@ pub const Cell = packed struct(u64) {
|
|||||||
/// Whether this was written with the protection flag set.
|
/// Whether this was written with the protection flag set.
|
||||||
protected: bool = false,
|
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) {
|
pub const ContentTag = enum(u2) {
|
||||||
/// A single codepoint, could be zero to be empty cell.
|
/// A single codepoint, could be zero to be empty cell.
|
||||||
|
Reference in New Issue
Block a user