mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-14 11:35:48 +00:00
osc: parse iTerm2 OSC 1337 extensions (#10417)
Add a framework for parsing iTerm2's OSC 1337 extensions. Implement a couple (`Copy` and `CurrentDir`) that map easily onto existing OSC commands.
This commit is contained in:
@@ -406,6 +406,7 @@ pub const Parser = struct {
|
||||
@"119",
|
||||
@"133",
|
||||
@"777",
|
||||
@"1337",
|
||||
};
|
||||
|
||||
pub fn init(alloc: ?Allocator) Parser {
|
||||
@@ -663,8 +664,20 @@ pub const Parser = struct {
|
||||
else => self.state = .invalid,
|
||||
},
|
||||
|
||||
.@"0",
|
||||
.@"133",
|
||||
=> switch (c) {
|
||||
';' => self.writeToFixed(),
|
||||
'7' => self.state = .@"1337",
|
||||
else => self.state = .invalid,
|
||||
},
|
||||
|
||||
.@"1337",
|
||||
=> switch (c) {
|
||||
';' => self.writeToFixed(),
|
||||
else => self.state = .invalid,
|
||||
},
|
||||
|
||||
.@"0",
|
||||
.@"22",
|
||||
.@"777",
|
||||
.@"8",
|
||||
@@ -741,6 +754,8 @@ pub const Parser = struct {
|
||||
.@"133" => parsers.semantic_prompt.parse(self, terminator_ch),
|
||||
|
||||
.@"777" => parsers.rxvt_extension.parse(self, terminator_ch),
|
||||
|
||||
.@"1337" => parsers.iterm2.parse(self, terminator_ch),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ pub const change_window_title = @import("parsers/change_window_title.zig");
|
||||
pub const clipboard_operation = @import("parsers/clipboard_operation.zig");
|
||||
pub const color = @import("parsers/color.zig");
|
||||
pub const hyperlink = @import("parsers/hyperlink.zig");
|
||||
pub const iterm2 = @import("parsers/iterm2.zig");
|
||||
pub const kitty_color = @import("parsers/kitty_color.zig");
|
||||
pub const kitty_text_sizing = @import("parsers/kitty_text_sizing.zig");
|
||||
pub const mouse_shape = @import("parsers/mouse_shape.zig");
|
||||
@@ -19,6 +20,7 @@ test {
|
||||
_ = clipboard_operation;
|
||||
_ = color;
|
||||
_ = hyperlink;
|
||||
_ = iterm2;
|
||||
_ = kitty_color;
|
||||
_ = kitty_text_sizing;
|
||||
_ = mouse_shape;
|
||||
|
||||
435
src/terminal/osc/parsers/iterm2.zig
Normal file
435
src/terminal/osc/parsers/iterm2.zig
Normal file
@@ -0,0 +1,435 @@
|
||||
const std = @import("std");
|
||||
|
||||
const assert = @import("../../../quirks.zig").inlineAssert;
|
||||
const simd = @import("../../../simd/main.zig");
|
||||
|
||||
const Parser = @import("../../osc.zig").Parser;
|
||||
const Command = @import("../../osc.zig").Command;
|
||||
|
||||
const log = std.log.scoped(.osc_iterm2);
|
||||
|
||||
const Key = enum {
|
||||
AddAnnotation,
|
||||
AddHiddenAnnotation,
|
||||
Block,
|
||||
Button,
|
||||
ClearCapturedOutput,
|
||||
ClearScrollback,
|
||||
Copy,
|
||||
CopyToClipboard,
|
||||
CurrentDir,
|
||||
CursorShape,
|
||||
Custom,
|
||||
Disinter,
|
||||
EndCopy,
|
||||
File,
|
||||
FileEnd,
|
||||
FilePart,
|
||||
HighlightCursorLine,
|
||||
MultipartFile,
|
||||
OpenURL,
|
||||
PopKeyLabels,
|
||||
PushKeyLabels,
|
||||
RemoteHost,
|
||||
ReportCellSize,
|
||||
ReportVariable,
|
||||
RequestAttention,
|
||||
RequestUpload,
|
||||
SetBackgroundImageFile,
|
||||
SetBadgeFormat,
|
||||
SetColors,
|
||||
SetKeyLabel,
|
||||
SetMark,
|
||||
SetProfile,
|
||||
SetUserVar,
|
||||
ShellIntegrationVersion,
|
||||
StealFocus,
|
||||
UnicodeVersion,
|
||||
};
|
||||
|
||||
// Instead of using `std.meta.stringToEnum` we set up a StaticStringMap so
|
||||
// that we can get ASCII case-insensitive lookups.
|
||||
const Map = std.StaticStringMapWithEql(Key, std.ascii.eqlIgnoreCase);
|
||||
const map: Map = .initComptime(
|
||||
map: {
|
||||
const fields = @typeInfo(Key).@"enum".fields;
|
||||
var tmp: [fields.len]struct { [:0]const u8, Key } = undefined;
|
||||
for (fields, 0..) |field, i| {
|
||||
tmp[i] = .{ field.name, @enumFromInt(field.value) };
|
||||
}
|
||||
break :map tmp;
|
||||
},
|
||||
);
|
||||
|
||||
/// Parse OSC 1337
|
||||
/// https://iterm2.com/documentation-escape-codes.html
|
||||
pub fn parse(parser: *Parser, _: ?u8) ?*Command {
|
||||
assert(parser.state == .@"1337");
|
||||
|
||||
const writer = parser.writer orelse {
|
||||
parser.state = .invalid;
|
||||
return null;
|
||||
};
|
||||
writer.writeByte(0) catch {
|
||||
parser.state = .invalid;
|
||||
return null;
|
||||
};
|
||||
const data = writer.buffered();
|
||||
|
||||
const key_str: [:0]u8, const value_: ?[:0]u8 = kv: {
|
||||
const index = std.mem.indexOfScalar(u8, data, '=') orelse {
|
||||
break :kv .{ data[0 .. data.len - 1 :0], null };
|
||||
};
|
||||
data[index] = 0;
|
||||
break :kv .{ data[0..index :0], data[index + 1 .. data.len - 1 :0] };
|
||||
};
|
||||
|
||||
const key = map.get(key_str) orelse {
|
||||
parser.command = .invalid;
|
||||
return null;
|
||||
};
|
||||
|
||||
switch (key) {
|
||||
.Copy => {
|
||||
var value = value_ orelse {
|
||||
parser.command = .invalid;
|
||||
return null;
|
||||
};
|
||||
|
||||
// Sending a blank entry to clear the clipboard is an OSC 52-ism,
|
||||
// make sure that is invalid here.
|
||||
if (value.len == 0) {
|
||||
parser.command = .invalid;
|
||||
return null;
|
||||
}
|
||||
|
||||
// base64 value must be prefixed by a colon
|
||||
if (value[0] != ':') {
|
||||
parser.command = .invalid;
|
||||
return null;
|
||||
}
|
||||
|
||||
value = value[1..value.len :0];
|
||||
|
||||
// Sending a blank entry to clear the clipboard is an OSC 52-ism,
|
||||
// make sure that is invalid here.
|
||||
if (value.len == 0) {
|
||||
parser.command = .invalid;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Sending a '?' to query the clipboard is an OSC 52-ism, make sure
|
||||
// that is invalid here.
|
||||
if (value.len == 1 and value[0] == '?') {
|
||||
parser.command = .invalid;
|
||||
return null;
|
||||
}
|
||||
|
||||
// It would be better to check for valid base64 data here, but that
|
||||
// would mean parsing the base64 data twice in the "normal" case.
|
||||
|
||||
parser.command = .{
|
||||
.clipboard_contents = .{
|
||||
.kind = 'c',
|
||||
.data = value,
|
||||
},
|
||||
};
|
||||
return &parser.command;
|
||||
},
|
||||
|
||||
.CurrentDir => {
|
||||
const value = value_ orelse {
|
||||
parser.command = .invalid;
|
||||
return null;
|
||||
};
|
||||
if (value.len == 0) {
|
||||
parser.command = .invalid;
|
||||
return null;
|
||||
}
|
||||
parser.command = .{
|
||||
.report_pwd = .{
|
||||
.value = value,
|
||||
},
|
||||
};
|
||||
return &parser.command;
|
||||
},
|
||||
|
||||
.AddAnnotation,
|
||||
.AddHiddenAnnotation,
|
||||
.Block,
|
||||
.Button,
|
||||
.ClearCapturedOutput,
|
||||
.ClearScrollback,
|
||||
.CopyToClipboard,
|
||||
.CursorShape,
|
||||
.Custom,
|
||||
.Disinter,
|
||||
.EndCopy,
|
||||
.File,
|
||||
.FileEnd,
|
||||
.FilePart,
|
||||
.HighlightCursorLine,
|
||||
.MultipartFile,
|
||||
.OpenURL,
|
||||
.PopKeyLabels,
|
||||
.PushKeyLabels,
|
||||
.RemoteHost,
|
||||
.ReportCellSize,
|
||||
.ReportVariable,
|
||||
.RequestAttention,
|
||||
.RequestUpload,
|
||||
.SetBackgroundImageFile,
|
||||
.SetBadgeFormat,
|
||||
.SetColors,
|
||||
.SetKeyLabel,
|
||||
.SetMark,
|
||||
.SetProfile,
|
||||
.SetUserVar,
|
||||
.ShellIntegrationVersion,
|
||||
.StealFocus,
|
||||
.UnicodeVersion,
|
||||
=> {
|
||||
log.debug("unimplemented OSC 1337: {t}", .{key});
|
||||
parser.command = .invalid;
|
||||
return null;
|
||||
},
|
||||
}
|
||||
return &parser.command;
|
||||
}
|
||||
|
||||
test "OSC: 1337: test valid unimplemented key with no value" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init(testing.allocator);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "1337;SetBadgeFormat";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
try testing.expect(p.end('\x1b') == null);
|
||||
}
|
||||
|
||||
test "OSC: 1337: test valid unimplemented key with empty value" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init(testing.allocator);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "1337;SetBadgeFormat=";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
try testing.expect(p.end('\x1b') == null);
|
||||
}
|
||||
|
||||
test "OSC: 1337: test valid unimplemented key with non-empty value" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init(testing.allocator);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "1337;SetBadgeFormat=abc123";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
try testing.expect(p.end('\x1b') == null);
|
||||
}
|
||||
|
||||
test "OSC: 1337: test valid key with lower case and with no value" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init(testing.allocator);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "1337;setbadgeformat";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
try testing.expect(p.end('\x1b') == null);
|
||||
}
|
||||
|
||||
test "OSC: 1337: test valid key with lower case and with empty value" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init(testing.allocator);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "1337;setbadgeformat=";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
try testing.expect(p.end('\x1b') == null);
|
||||
}
|
||||
|
||||
test "OSC: 1337: test valid key with lower case and with non-empty value" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init(testing.allocator);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "1337;setbadgeformat=abc123";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
try testing.expect(p.end('\x1b') == null);
|
||||
}
|
||||
|
||||
test "OSC: 1337: test invalid key with no value" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init(testing.allocator);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "1337;BobrKurwa";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
try testing.expect(p.end('\x1b') == null);
|
||||
}
|
||||
|
||||
test "OSC: 1337: test invalid key with empty value" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init(testing.allocator);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "1337;BobrKurwa=";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
try testing.expect(p.end('\x1b') == null);
|
||||
}
|
||||
|
||||
test "OSC: 1337: test invalid key with non-empty value" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init(testing.allocator);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "1337;BobrKurwa=abc123";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
try testing.expect(p.end('\x1b') == null);
|
||||
}
|
||||
|
||||
test "OSC: 1337: test Copy with no value" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init(testing.allocator);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "1337;Copy";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
try testing.expect(p.end('\x1b') == null);
|
||||
}
|
||||
|
||||
test "OSC: 1337: test Copy with empty value" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init(testing.allocator);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "1337;Copy=";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
try testing.expect(p.end('\x1b') == null);
|
||||
}
|
||||
|
||||
test "OSC: 1337: test Copy with only prefix colon" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init(testing.allocator);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "1337;Copy=:";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
try testing.expect(p.end('\x1b') == null);
|
||||
}
|
||||
|
||||
test "OSC: 1337: test Copy with question mark" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init(testing.allocator);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "1337;Copy=:?";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
try testing.expect(p.end('\x1b') == null);
|
||||
}
|
||||
|
||||
test "OSC: 1337: test Copy with non-empty value that is invalid base64" {
|
||||
// For performance reasons, we don't check for valid base64 data
|
||||
// right now.
|
||||
return error.SkipZigTest;
|
||||
|
||||
// const testing = std.testing;
|
||||
|
||||
// var p: Parser = .init(testing.allocator);
|
||||
// defer p.deinit();
|
||||
|
||||
// const input = "1337;Copy=:abc123";
|
||||
// for (input) |ch| p.next(ch);
|
||||
|
||||
// try testing.expect(p.end('\x1b') == null);
|
||||
}
|
||||
|
||||
test "OSC: 1337: test Copy with non-empty value that is valid base64 but not prefixed with a colon" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init(testing.allocator);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "1337;Copy=YWJjMTIz";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
try testing.expect(p.end('\x1b') == null);
|
||||
}
|
||||
|
||||
test "OSC: 1337: test Copy with non-empty value that is valid base64" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init(testing.allocator);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "1337;Copy=:YWJjMTIz";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?.*;
|
||||
try testing.expect(cmd == .clipboard_contents);
|
||||
try testing.expectEqual('c', cmd.clipboard_contents.kind);
|
||||
try testing.expectEqualStrings("YWJjMTIz", cmd.clipboard_contents.data);
|
||||
}
|
||||
|
||||
test "OSC: 1337: test CurrentDir with no value" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init(testing.allocator);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "1337;CurrentDir";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
try testing.expect(p.end('\x1b') == null);
|
||||
}
|
||||
|
||||
test "OSC: 1337: test CurrentDir with empty value" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init(testing.allocator);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "1337;CurrentDir=";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
try testing.expect(p.end('\x1b') == null);
|
||||
}
|
||||
|
||||
test "OSC: 1337: test CurrentDir with non-empty value" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .init(testing.allocator);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "1337;CurrentDir=abc123";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?.*;
|
||||
try testing.expect(cmd == .report_pwd);
|
||||
try testing.expectEqualStrings("abc123", cmd.report_pwd.value);
|
||||
}
|
||||
Reference in New Issue
Block a user