termio: implement DECRQSS

Only SGR, DECSCUSR, DECSTBM, and DECSLRM are handled, as these are the
only ones that Ghostty supports (as far as I can tell) and are the only
ones that seem actually useful.
This commit is contained in:
Gregory Anders
2023-12-14 14:48:12 -06:00
parent cc6270f549
commit bf06c05c09
3 changed files with 276 additions and 2 deletions

View File

@ -148,7 +148,7 @@ pub const MouseFormat = enum(u3) {
}; };
/// Scrolling region is the area of the screen designated where scrolling /// Scrolling region is the area of the screen designated where scrolling
/// occurs. Wen scrolling the screen, only this viewport is scrolled. /// occurs. When scrolling the screen, only this viewport is scrolled.
pub const ScrollingRegion = struct { pub const ScrollingRegion = struct {
// Top and bottom of the scroll region (0-indexed) // Top and bottom of the scroll region (0-indexed)
// Precondition: top < bottom // Precondition: top < bottom
@ -613,6 +613,77 @@ pub fn setAttribute(self: *Terminal, attr: sgr.Attribute) !void {
} }
} }
/// Print the active attributes as a string. This is used to respond to DECRQSS
/// requests.
///
/// Boolean attributes are printed first, followed by foreground color, then
/// background color. Each attribute is separated by a semicolon.
pub fn printAttributes(self: *Terminal, buf: []u8) ![]const u8 {
var stream = std.io.fixedBufferStream(buf);
const writer = stream.writer();
// The SGR response always starts with a 0. See https://vt100.net/docs/vt510-rm/DECRPSS
try writer.writeByte('0');
const pen = self.screen.cursor.pen;
var attrs = [_]u8{0} ** 8;
var i: usize = 0;
if (pen.attrs.bold) {
attrs[i] = '1';
i += 1;
}
if (pen.attrs.faint) {
attrs[i] = '2';
i += 1;
}
if (pen.attrs.italic) {
attrs[i] = '3';
i += 1;
}
if (pen.attrs.underline != .none) {
attrs[i] = '4';
i += 1;
}
if (pen.attrs.blink) {
attrs[i] = '5';
i += 1;
}
if (pen.attrs.inverse) {
attrs[i] = '7';
i += 1;
}
if (pen.attrs.invisible) {
attrs[i] = '8';
i += 1;
}
if (pen.attrs.strikethrough) {
attrs[i] = '9';
i += 1;
}
for (attrs[0..i]) |c| {
try writer.print(";{c}", .{c});
}
if (pen.attrs.has_fg) {
try writer.print(";38:2::{[r]}:{[g]}:{[b]}", pen.fg);
}
if (pen.attrs.has_bg) {
try writer.print(";48:2::{[r]}:{[g]}:{[b]}", pen.bg);
}
return stream.getWritten();
}
/// Set the charset into the given slot. /// Set the charset into the given slot.
pub fn configureCharset(self: *Terminal, slot: charsets.Slots, set: charsets.Charset) void { pub fn configureCharset(self: *Terminal, slot: charsets.Slots, set: charsets.Charset) void {
self.screen.charset.charsets.set(slot, set); self.screen.charset.charsets.set(slot, set);
@ -6950,3 +7021,54 @@ test "Terminal: DECCOLM resets scroll region" {
try testing.expectEqual(@as(usize, 0), t.scrolling_region.left); try testing.expectEqual(@as(usize, 0), t.scrolling_region.left);
try testing.expectEqual(@as(usize, 79), t.scrolling_region.right); try testing.expectEqual(@as(usize, 79), t.scrolling_region.right);
} }
test "Terminal: printAttributes" {
const alloc = testing.allocator;
var t = try init(alloc, 5, 5);
defer t.deinit(alloc);
var storage: [64]u8 = undefined;
{
try t.setAttribute(.{ .direct_color_fg = .{ .r = 1, .g = 2, .b = 3 } });
defer t.setAttribute(.unset) catch unreachable;
const buf = try t.printAttributes(&storage);
try testing.expectEqualStrings("0;38:2::1:2:3", buf);
}
{
try t.setAttribute(.bold);
try t.setAttribute(.{ .direct_color_bg = .{ .r = 1, .g = 2, .b = 3 } });
defer t.setAttribute(.unset) catch unreachable;
const buf = try t.printAttributes(&storage);
try testing.expectEqualStrings("0;1;48:2::1:2:3", buf);
}
{
try t.setAttribute(.bold);
try t.setAttribute(.faint);
try t.setAttribute(.italic);
try t.setAttribute(.{ .underline = .single });
try t.setAttribute(.blink);
try t.setAttribute(.inverse);
try t.setAttribute(.invisible);
try t.setAttribute(.strikethrough);
try t.setAttribute(.{ .direct_color_fg = .{ .r = 100, .g = 200, .b = 255 } });
try t.setAttribute(.{ .direct_color_bg = .{ .r = 101, .g = 102, .b = 103 } });
defer t.setAttribute(.unset) catch unreachable;
const buf = try t.printAttributes(&storage);
try testing.expectEqualStrings("0;1;2;3;4;5;7;8;9;38:2::100:200:255;48:2::101:102:103", buf);
}
{
try t.setAttribute(.{ .underline = .single });
defer t.setAttribute(.unset) catch unreachable;
const buf = try t.printAttributes(&storage);
try testing.expectEqualStrings("0;4", buf);
}
{
const buf = try t.printAttributes(&storage);
try testing.expectEqualStrings("0", buf);
}
}

View File

@ -53,6 +53,15 @@ pub const Handler = struct {
else => null, else => null,
}, },
'$' => switch (dcs.final) {
// DECRQSS
'q' => .{
.decrqss = .{},
},
else => null,
},
else => null, else => null,
}, },
@ -82,6 +91,15 @@ pub const Handler = struct {
try list.append(byte); try list.append(byte);
}, },
.decrqss => |*buffer| {
if (buffer.len >= buffer.data.len) {
return error.OutOfMemory;
}
buffer.data[buffer.len] = byte;
buffer.len += 1;
},
} }
} }
@ -93,6 +111,24 @@ pub const Handler = struct {
=> null, => null,
.xtgettcap => |list| .{ .xtgettcap = .{ .data = list } }, .xtgettcap => |list| .{ .xtgettcap = .{ .data = list } },
.decrqss => |buffer| .{ .decrqss = switch (buffer.len) {
0 => .none,
1 => switch (buffer.data[0]) {
'm' => .sgr,
'r' => .decstbm,
's' => .decslrm,
else => .none,
},
2 => switch (buffer.data[0]) {
' ' => switch (buffer.data[1]) {
'q' => .decscusr,
else => .none,
},
else => .none,
},
else => unreachable,
} },
}; };
} }
@ -103,6 +139,8 @@ pub const Handler = struct {
=> {}, => {},
.xtgettcap => |*list| list.deinit(), .xtgettcap => |*list| list.deinit(),
.decrqss => {},
} }
self.state = .{ .inactive = {} }; self.state = .{ .inactive = {} };
@ -113,11 +151,15 @@ pub const Command = union(enum) {
/// XTGETTCAP /// XTGETTCAP
xtgettcap: XTGETTCAP, xtgettcap: XTGETTCAP,
/// DECRQSS
decrqss: DECRQSS,
pub fn deinit(self: Command) void { pub fn deinit(self: Command) void {
switch (self) { switch (self) {
.xtgettcap => |*v| { .xtgettcap => |*v| {
v.data.deinit(); v.data.deinit();
}, },
.decrqss => {},
} }
} }
@ -142,6 +184,15 @@ pub const Command = union(enum) {
return rem[0..idx]; return rem[0..idx];
} }
}; };
/// Supported DECRQSS settings
pub const DECRQSS = enum {
none,
sgr,
decscusr,
decstbm,
decslrm,
};
}; };
const State = union(enum) { const State = union(enum) {
@ -152,8 +203,14 @@ const State = union(enum) {
/// invalid due to some bad input, so we're ignoring the rest. /// invalid due to some bad input, so we're ignoring the rest.
ignore: void, ignore: void,
// XTGETTCAP /// XTGETTCAP
xtgettcap: std.ArrayList(u8), xtgettcap: std.ArrayList(u8),
/// DECRQSS
decrqss: struct {
data: [2]u8 = undefined,
len: usize = 0,
},
}; };
test "unknown DCS command" { test "unknown DCS command" {
@ -214,3 +271,39 @@ test "XTGETTCAP command invalid data" {
try testing.expectEqualStrings("536D756C78", cmd.xtgettcap.next().?); try testing.expectEqualStrings("536D756C78", cmd.xtgettcap.next().?);
try testing.expect(cmd.xtgettcap.next() == null); try testing.expect(cmd.xtgettcap.next() == null);
} }
test "DECRQSS command" {
const testing = std.testing;
const alloc = testing.allocator;
var h: Handler = .{};
defer h.deinit();
h.hook(alloc, .{ .intermediates = "$", .final = 'q' });
h.put('m');
var cmd = h.unhook().?;
defer cmd.deinit();
try testing.expect(cmd == .decrqss);
try testing.expect(cmd.decrqss == .sgr);
}
test "DECRQSS invalid command" {
const testing = std.testing;
const alloc = testing.allocator;
var h: Handler = .{};
defer h.deinit();
h.hook(alloc, .{ .intermediates = "$", .final = 'q' });
h.put('z');
var cmd = h.unhook().?;
defer cmd.deinit();
try testing.expect(cmd == .decrqss);
try testing.expect(cmd.decrqss == .none);
h.discard();
h.hook(alloc, .{ .intermediates = "$", .final = 'q' });
h.put('"');
h.put(' ');
h.put('q');
try testing.expect(h.unhook() == null);
}

View File

@ -1485,6 +1485,65 @@ const StreamHandler = struct {
self.messageWriter(.{ .write_stable = response }); self.messageWriter(.{ .write_stable = response });
} }
}, },
.decrqss => |decrqss| {
var response: [128]u8 = undefined;
var stream = std.io.fixedBufferStream(&response);
const writer = stream.writer();
switch (decrqss) {
// Invalid or unhandled request
.none => try writer.writeAll("\x1bP0$r"),
// DECSLRM is special because we send a valid response
// when left and right margin mode (DECLRMM) is enabled.
.decslrm => {
if (self.terminal.modes.get(.enable_left_and_right_margin)) {
try writer.print("\x1bP1$r{d};{d}s", .{
self.terminal.scrolling_region.left + 1,
self.terminal.scrolling_region.right + 1,
});
} else {
try writer.writeAll("\x1bP0$r");
}
},
else => {
try writer.writeAll("\x1bP1$r");
switch (decrqss) {
.sgr => {
const buf = try self.terminal.printAttributes(stream.buffer[stream.pos..]);
// printAttributes wrote into our buffer, so adjust the stream
// position
stream.pos += buf.len;
try writer.writeByte('m');
},
.decscusr => {
const blink = self.terminal.modes.get(.cursor_blinking);
const style: u8 = switch (self.terminal.screen.cursor.style) {
.block => if (blink) 1 else 2,
.underline => if (blink) 3 else 4,
.bar => if (blink) 5 else 6,
};
try writer.print("{d} q", .{style});
},
.decstbm => {
try writer.print("{d};{d}r", .{
self.terminal.scrolling_region.top + 1,
self.terminal.scrolling_region.bottom + 1,
});
},
.decslrm,
.none,
=> unreachable,
}
},
}
try writer.writeAll("\x1b\\");
const msg = try termio.Message.writeReq(self.alloc, response[0..stream.pos]);
self.messageWriter(msg);
},
} }
} }