terminal: add device_attributes module

Introduce a dedicated device_attributes.zig module that consolidates
all device attribute types and encoding logic. This moves
DeviceAttributeReq out of ansi.zig and adds structured response
types for DA1 (primary), DA2 (secondary), and DA3 (tertiary) with
self-encoding methods.

Primary DA uses a ConformanceLevel enum covering VT100-series
per-model values and VT200+ conformance levels, plus a Feature
enum with all known xterm DA1 attribute codes (132-col, printer,
sixel, color, clipboard, etc.) as a simple slice. Secondary DA
uses a DeviceType enum matching the xterm decTerminalID values.
Tertiary DA encodes the DECRPTUI unit ID as a u32 formatted to
8 hex digits.

This is preparatory work for exposing device attributes through
the stream_terminal Effects callback system.
This commit is contained in:
Mitchell Hashimoto
2026-03-23 14:35:22 -07:00
parent 2e7aa047af
commit b31dcf9a4c
4 changed files with 221 additions and 13 deletions

View File

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

View File

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

View File

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

View File

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