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 cap = if (parser.capture) |*c| c else { parser.state = .invalid; return null; }; cap.writer.writeByte(0) catch { parser.state = .invalid; return null; }; const data = cap.trailing(); 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); }