diff --git a/src/terminal/ansi.zig b/src/terminal/ansi.zig index c9cd53666..86b00c801 100644 --- a/src/terminal/ansi.zig +++ b/src/terminal/ansi.zig @@ -52,16 +52,6 @@ pub const RenditionAspect = enum(u16) { _, }; -/// The device attribute request type (ESC [ c). -pub const DeviceAttributeReq = lib.Enum( - lib_target, - &.{ - "primary", // Blank - "secondary", // > - "tertiary", // = - }, -); - /// Possible cursor styles (ESC [ q) pub const CursorStyle = lib.Enum( lib_target, diff --git a/src/terminal/device_attributes.zig b/src/terminal/device_attributes.zig new file mode 100644 index 000000000..b7844b60d --- /dev/null +++ b/src/terminal/device_attributes.zig @@ -0,0 +1,216 @@ +const std = @import("std"); +const testing = std.testing; +const build_options = @import("terminal_options"); +const lib = @import("../lib/main.zig"); +const lib_target: lib.Target = if (build_options.c_abi) .c else .zig; + +/// The device attribute request type (CSI c). +pub const Req = lib.Enum(lib_target, &.{ + "primary", // Blank + "secondary", // > + "tertiary", // = +}); + +/// Response data for all device attribute queries. +pub const Response = struct { + /// Reply to CSI c (DA1). + primary: Primary = .{}, + + /// Reply to CSI > c (DA2). + secondary: Secondary = .{}, + + /// Reply to CSI = c (DA3). + tertiary: Tertiary = .{}, +}; + +/// Primary device attributes (DA1). +/// +/// Response format: CSI ? Pp ; Ps... c +/// where Pp is the conformance level and Ps are feature flags. +pub const Primary = struct { + /// Conformance level sent as the first parameter. + conformance_level: ConformanceLevel = .vt220, + + /// Optional feature attributes. + features: []const Feature = &.{.ansi_color}, + + /// DA1 feature attribute codes. + pub const Feature = enum(u16) { + columns_132 = 1, + printer = 2, + regis = 3, + sixel = 4, + selective_erase = 6, + user_defined_keys = 8, + national_replacement = 9, + technical_characters = 15, + locator = 16, + terminal_state = 17, + windowing = 18, + horizontal_scrolling = 21, + ansi_color = 22, + rectangular_editing = 28, + ansi_text_locator = 29, + clipboard = 52, + _, + }; + + /// Encode the primary DA response into the writer. + pub fn encode(self: Primary, writer: *std.Io.Writer) std.Io.Writer.Error!void { + try writer.print("\x1b[?{}", .{@intFromEnum(self.conformance_level)}); + for (self.features) |feature| try writer.print(";{}", .{@intFromEnum(feature)}); + try writer.writeAll("c"); + } +}; + +/// Secondary device attributes (DA2). +/// +/// Response format: CSI > Pp ; Pv ; Pc c +pub const Secondary = struct { + /// Terminal type identifier (Pp parameter from secondary DA response). + device_type: DeviceType = .vt220, + + /// Firmware/patch version number. + firmware_version: u16 = 0, + + /// ROM cartridge registration number. Always 0 for emulators. + rom_cartridge: u16 = 0, + + /// Encode the secondary DA response into the writer. + pub fn encode(self: Secondary, writer: *std.Io.Writer) std.Io.Writer.Error!void { + try writer.print("\x1b[>{};{};{}c", .{ + @intFromEnum(self.device_type), + self.firmware_version, + self.rom_cartridge, + }); + } +}; + +/// Tertiary device attributes (DA3). +/// +/// Response format: DCS ! | D...D ST +/// where D...D is the unit ID as hex digits (DECRPTUI). +pub const Tertiary = struct { + /// Unit ID (DECRPTUI). Encoded as 8 uppercase hex digits. + /// Meaningless for emulators nowadays. The actual DEC manuals + /// appear to split this into two 16-bit fields but since there + /// is no practical usage I know if I'm simplifying this. + unit_id: u32 = 0, + + /// Encode the tertiary DA response into the writer. + pub fn encode( + self: Tertiary, + writer: *std.Io.Writer, + ) std.Io.Writer.Error!void { + try writer.print( + "\x1bP!|{X:0>8}\x1b\\", + .{self.unit_id}, + ); + } +}; + +/// Conformance level reported as the first parameter (Pp) in the +/// primary device attributes (DA1) response. +pub const ConformanceLevel = enum(u16) { + // VT100-series have per-model values. + vt100 = 1, + vt132 = 4, + vt102 = 6, + vt131 = 7, + vt125 = 12, + + // VT200+ use 60 + decTerminalID/100. + /// Level 2 conformance (VT200 series, e.g. VT220, VT240). + level_2 = 62, + /// Level 3 conformance (VT300 series, e.g. VT320, VT340). + level_3 = 63, + /// Level 4 conformance (VT400 series, e.g. VT420). + level_4 = 64, + /// Level 5 conformance (VT500 series, e.g. VT510, VT520, VT525). + level_5 = 65, + + _, + + pub const vt101 = ConformanceLevel.vt100; + pub const vt220 = ConformanceLevel.level_2; + pub const vt240 = ConformanceLevel.level_2; + pub const vt320 = ConformanceLevel.level_3; + pub const vt340 = ConformanceLevel.level_3; + pub const vt420 = ConformanceLevel.level_4; + pub const vt510 = ConformanceLevel.level_5; + pub const vt520 = ConformanceLevel.level_5; + pub const vt525 = ConformanceLevel.level_5; +}; + +/// Terminal type identifier reported as the Pp parameter in the +/// secondary device attributes (DA2) response. Values correspond +/// to the decTerminalID resource in xterm. +pub const DeviceType = enum(u16) { + vt100 = 0, + vt220 = 1, + vt240 = 2, + vt330 = 18, + vt340 = 19, + vt320 = 24, + vt382 = 32, + vt420 = 41, + vt510 = 61, + vt520 = 64, + vt525 = 65, + _, +}; + +test "primary default" { + var buf: [64]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try (Primary{}).encode(&writer); + try testing.expectEqualStrings("\x1b[?62;22c", writer.buffered()); +} + +test "primary with clipboard" { + var buf: [64]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try (Primary{ .features = &.{ .ansi_color, .clipboard } }).encode(&writer); + try testing.expectEqualStrings("\x1b[?62;22;52c", writer.buffered()); +} + +test "primary with multiple features" { + var buf: [64]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try (Primary{ + .conformance_level = .vt420, + .features = &.{ .columns_132, .selective_erase, .ansi_color }, + }).encode(&writer); + try testing.expectEqualStrings("\x1b[?64;1;6;22c", writer.buffered()); +} + +test "primary no features" { + var buf: [64]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try (Primary{ + .conformance_level = .vt100, + .features = &.{}, + }).encode(&writer); + try testing.expectEqualStrings("\x1b[?1c", writer.buffered()); +} + +test "secondary default" { + var buf: [64]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try (Secondary{}).encode(&writer); + try testing.expectEqualStrings("\x1b[>1;10;0c", writer.buffered()); +} + +test "tertiary default" { + var buf: [64]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try (Tertiary{}).encode(&writer); + try testing.expectEqualStrings("\x1bP!|00000000\x1b\\", writer.buffered()); +} + +test "tertiary custom unit id" { + var buf: [64]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try (Tertiary{ .unit_id = 0xAABBCCDD }).encode(&writer); + try testing.expectEqualStrings("\x1bP!|AABBCCDD\x1b\\", writer.buffered()); +} diff --git a/src/terminal/main.zig b/src/terminal/main.zig index e58c828b8..9f5b65e34 100644 --- a/src/terminal/main.zig +++ b/src/terminal/main.zig @@ -10,6 +10,7 @@ pub const dcs = @import("dcs.zig"); pub const osc = @import("osc.zig"); pub const point = @import("point.zig"); pub const color = @import("color.zig"); +pub const device_attributes = @import("device_attributes.zig"); pub const device_status = @import("device_status.zig"); pub const focus = @import("focus.zig"); pub const formatter = @import("formatter.zig"); @@ -57,7 +58,7 @@ pub const StreamAction = stream.Action; pub const Cursor = Screen.Cursor; pub const CursorStyle = Screen.CursorStyle; pub const CursorStyleReq = ansi.CursorStyle; -pub const DeviceAttributeReq = ansi.DeviceAttributeReq; +pub const DeviceAttributeReq = device_attributes.Req; pub const Mode = modes.Mode; pub const ModePacked = modes.ModePacked; pub const ModifyKeyFormat = ansi.ModifyKeyFormat; diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index c582635b1..04410e94c 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -9,6 +9,7 @@ const lib = @import("../lib/main.zig"); const Parser = @import("Parser.zig"); const ansi = @import("ansi.zig"); const charsets = @import("charsets.zig"); +const device_attributes = @import("device_attributes.zig"); const device_status = @import("device_status.zig"); const csi = @import("csi.zig"); const kitty = @import("kitty.zig"); @@ -97,7 +98,7 @@ pub const Action = union(Key) { title_push: u16, title_pop: u16, xtversion, - device_attributes: ansi.DeviceAttributeReq, + device_attributes: device_attributes.Req, device_status: DeviceStatus, kitty_keyboard_query, kitty_keyboard_push: KittyKeyboardFlags, @@ -1282,7 +1283,7 @@ pub fn Stream(comptime H: type) type { // c - Device Attributes (DA1) 'c' => { - const req: ?ansi.DeviceAttributeReq = switch (input.intermediates.len) { + const req: ?device_attributes.Req = switch (input.intermediates.len) { 0 => .primary, 1 => switch (input.intermediates[0]) { '>' => .secondary,