mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +03:00
terminal: parse osc 8 hyperlink_start
This commit is contained in:
@ -133,6 +133,12 @@ pub const Command = union(enum) {
|
|||||||
body: []const u8,
|
body: []const u8,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Start a hyperlink (OSC 8)
|
||||||
|
hyperlink_start: struct {
|
||||||
|
id: ?[]const u8 = null,
|
||||||
|
uri: []const u8,
|
||||||
|
},
|
||||||
|
|
||||||
pub const ColorKind = union(enum) {
|
pub const ColorKind = union(enum) {
|
||||||
palette: u8,
|
palette: u8,
|
||||||
foreground,
|
foreground,
|
||||||
@ -239,6 +245,7 @@ pub const Parser = struct {
|
|||||||
@"7",
|
@"7",
|
||||||
@"77",
|
@"77",
|
||||||
@"777",
|
@"777",
|
||||||
|
@"8",
|
||||||
@"9",
|
@"9",
|
||||||
|
|
||||||
// OSC 10 is used to query or set the current foreground color.
|
// OSC 10 is used to query or set the current foreground color.
|
||||||
@ -267,6 +274,10 @@ pub const Parser = struct {
|
|||||||
color_palette_index,
|
color_palette_index,
|
||||||
color_palette_index_end,
|
color_palette_index_end,
|
||||||
|
|
||||||
|
// Hyperlinks
|
||||||
|
hyperlink_param_key,
|
||||||
|
hyperlink_param_value,
|
||||||
|
|
||||||
// Reset color palette index
|
// Reset color palette index
|
||||||
reset_color_palette_index,
|
reset_color_palette_index,
|
||||||
|
|
||||||
@ -333,6 +344,7 @@ pub const Parser = struct {
|
|||||||
'4' => self.state = .@"4",
|
'4' => self.state = .@"4",
|
||||||
'5' => self.state = .@"5",
|
'5' => self.state = .@"5",
|
||||||
'7' => self.state = .@"7",
|
'7' => self.state = .@"7",
|
||||||
|
'8' => self.state = .@"8",
|
||||||
'9' => self.state = .@"9",
|
'9' => self.state = .@"9",
|
||||||
else => self.state = .invalid,
|
else => self.state = .invalid,
|
||||||
},
|
},
|
||||||
@ -556,6 +568,47 @@ pub const Parser = struct {
|
|||||||
else => self.state = .invalid,
|
else => self.state = .invalid,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
.@"8" => switch (c) {
|
||||||
|
';' => {
|
||||||
|
self.command = .{ .hyperlink_start = .{
|
||||||
|
.uri = "",
|
||||||
|
} };
|
||||||
|
|
||||||
|
self.state = .hyperlink_param_key;
|
||||||
|
self.buf_start = self.buf_idx;
|
||||||
|
},
|
||||||
|
else => self.state = .invalid,
|
||||||
|
},
|
||||||
|
|
||||||
|
.hyperlink_param_key => switch (c) {
|
||||||
|
';' => {
|
||||||
|
self.state = .string;
|
||||||
|
self.temp_state = .{ .str = &self.command.hyperlink_start.uri };
|
||||||
|
self.buf_start = self.buf_idx;
|
||||||
|
},
|
||||||
|
'=' => {
|
||||||
|
self.temp_state = .{ .key = self.buf[self.buf_start .. self.buf_idx - 1] };
|
||||||
|
self.state = .hyperlink_param_value;
|
||||||
|
self.buf_start = self.buf_idx;
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
},
|
||||||
|
|
||||||
|
.hyperlink_param_value => switch (c) {
|
||||||
|
':' => {
|
||||||
|
self.endHyperlinkOptionValue();
|
||||||
|
self.state = .hyperlink_param_key;
|
||||||
|
self.buf_start = self.buf_idx;
|
||||||
|
},
|
||||||
|
';' => {
|
||||||
|
self.endHyperlinkOptionValue();
|
||||||
|
self.state = .string;
|
||||||
|
self.temp_state = .{ .str = &self.command.hyperlink_start.uri };
|
||||||
|
self.buf_start = self.buf_idx;
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
},
|
||||||
|
|
||||||
.rxvt_extension => switch (c) {
|
.rxvt_extension => switch (c) {
|
||||||
'a'...'z' => {},
|
'a'...'z' => {},
|
||||||
';' => {
|
';' => {
|
||||||
@ -772,6 +825,24 @@ pub const Parser = struct {
|
|||||||
self.state = .allocable_string;
|
self.state = .allocable_string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn endHyperlinkOptionValue(self: *Parser) void {
|
||||||
|
const value = if (self.buf_start == self.buf_idx)
|
||||||
|
""
|
||||||
|
else
|
||||||
|
self.buf[self.buf_start .. self.buf_idx - 1];
|
||||||
|
|
||||||
|
if (mem.eql(u8, self.temp_state.key, "id")) {
|
||||||
|
switch (self.command) {
|
||||||
|
.hyperlink_start => |*v| {
|
||||||
|
// We treat empty IDs as null ids so that we can
|
||||||
|
// auto-assign.
|
||||||
|
if (value.len > 0) v.id = value;
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
} else log.info("unknown hyperlink option: {s}", .{self.temp_state.key});
|
||||||
|
}
|
||||||
|
|
||||||
fn endSemanticOptionValue(self: *Parser) void {
|
fn endSemanticOptionValue(self: *Parser) void {
|
||||||
const value = self.buf[self.buf_start..self.buf_idx];
|
const value = self.buf[self.buf_start..self.buf_idx];
|
||||||
|
|
||||||
@ -1272,3 +1343,86 @@ test "OSC: empty param" {
|
|||||||
const cmd = p.end('\x1b');
|
const cmd = p.end('\x1b');
|
||||||
try testing.expect(cmd == null);
|
try testing.expect(cmd == null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "OSC: hyperlink" {
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
var p: Parser = .{};
|
||||||
|
|
||||||
|
const input = "8;;http://example.com";
|
||||||
|
for (input) |ch| p.next(ch);
|
||||||
|
|
||||||
|
const cmd = p.end('\x1b').?;
|
||||||
|
try testing.expect(cmd == .hyperlink_start);
|
||||||
|
try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com");
|
||||||
|
}
|
||||||
|
|
||||||
|
test "OSC: hyperlink with id set" {
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
var p: Parser = .{};
|
||||||
|
|
||||||
|
const input = "8;id=foo;http://example.com";
|
||||||
|
for (input) |ch| p.next(ch);
|
||||||
|
|
||||||
|
const cmd = p.end('\x1b').?;
|
||||||
|
try testing.expect(cmd == .hyperlink_start);
|
||||||
|
try testing.expectEqualStrings(cmd.hyperlink_start.id.?, "foo");
|
||||||
|
try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com");
|
||||||
|
}
|
||||||
|
|
||||||
|
test "OSC: hyperlink with empty id" {
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
var p: Parser = .{};
|
||||||
|
|
||||||
|
const input = "8;id=;http://example.com";
|
||||||
|
for (input) |ch| p.next(ch);
|
||||||
|
|
||||||
|
const cmd = p.end('\x1b').?;
|
||||||
|
try testing.expect(cmd == .hyperlink_start);
|
||||||
|
try testing.expectEqual(null, cmd.hyperlink_start.id);
|
||||||
|
try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com");
|
||||||
|
}
|
||||||
|
|
||||||
|
test "OSC: hyperlink with incomplete key" {
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
var p: Parser = .{};
|
||||||
|
|
||||||
|
const input = "8;id;http://example.com";
|
||||||
|
for (input) |ch| p.next(ch);
|
||||||
|
|
||||||
|
const cmd = p.end('\x1b').?;
|
||||||
|
try testing.expect(cmd == .hyperlink_start);
|
||||||
|
try testing.expectEqual(null, cmd.hyperlink_start.id);
|
||||||
|
try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com");
|
||||||
|
}
|
||||||
|
|
||||||
|
test "OSC: hyperlink with empty key" {
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
var p: Parser = .{};
|
||||||
|
|
||||||
|
const input = "8;=value;http://example.com";
|
||||||
|
for (input) |ch| p.next(ch);
|
||||||
|
|
||||||
|
const cmd = p.end('\x1b').?;
|
||||||
|
try testing.expect(cmd == .hyperlink_start);
|
||||||
|
try testing.expectEqual(null, cmd.hyperlink_start.id);
|
||||||
|
try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com");
|
||||||
|
}
|
||||||
|
|
||||||
|
test "OSC: hyperlink with empty key and id" {
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
var p: Parser = .{};
|
||||||
|
|
||||||
|
const input = "8;=value:id=foo;http://example.com";
|
||||||
|
for (input) |ch| p.next(ch);
|
||||||
|
|
||||||
|
const cmd = p.end('\x1b').?;
|
||||||
|
try testing.expect(cmd == .hyperlink_start);
|
||||||
|
try testing.expectEqualStrings(cmd.hyperlink_start.id.?, "foo");
|
||||||
|
try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com");
|
||||||
|
}
|
||||||
|
@ -1333,6 +1333,11 @@ pub fn Stream(comptime Handler: type) type {
|
|||||||
return;
|
return;
|
||||||
} else log.warn("unimplemented OSC callback: {}", .{cmd});
|
} else log.warn("unimplemented OSC callback: {}", .{cmd});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
.hyperlink_start => |v| {
|
||||||
|
_ = v;
|
||||||
|
@panic("TODO(osc8)");
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall through for when we don't have a handler.
|
// Fall through for when we don't have a handler.
|
||||||
|
Reference in New Issue
Block a user