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:
Mitchell Hashimoto
2026-01-23 08:13:30 -08:00
committed by GitHub
3 changed files with 453 additions and 1 deletions

View File

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

View File

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

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