From bf06c05c09766a15ab70b52de8e4a49c32f42623 Mon Sep 17 00:00:00 2001 From: Gregory Anders Date: Thu, 14 Dec 2023 14:48:12 -0600 Subject: [PATCH] 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. --- src/terminal/Terminal.zig | 124 +++++++++++++++++++++++++++++++++++++- src/terminal/dcs.zig | 95 ++++++++++++++++++++++++++++- src/termio/Exec.zig | 59 ++++++++++++++++++ 3 files changed, 276 insertions(+), 2 deletions(-) diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 6cf454969..54086c903 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -148,7 +148,7 @@ pub const MouseFormat = enum(u3) { }; /// 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 { // Top and bottom of the scroll region (0-indexed) // 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. pub fn configureCharset(self: *Terminal, slot: charsets.Slots, set: charsets.Charset) void { 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, 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); + } +} diff --git a/src/terminal/dcs.zig b/src/terminal/dcs.zig index 83d4c7d22..197af2230 100644 --- a/src/terminal/dcs.zig +++ b/src/terminal/dcs.zig @@ -53,6 +53,15 @@ pub const Handler = struct { else => null, }, + '$' => switch (dcs.final) { + // DECRQSS + 'q' => .{ + .decrqss = .{}, + }, + + else => null, + }, + else => null, }, @@ -82,6 +91,15 @@ pub const Handler = struct { 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, .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(), + + .decrqss => {}, } self.state = .{ .inactive = {} }; @@ -113,11 +151,15 @@ pub const Command = union(enum) { /// XTGETTCAP xtgettcap: XTGETTCAP, + /// DECRQSS + decrqss: DECRQSS, + pub fn deinit(self: Command) void { switch (self) { .xtgettcap => |*v| { v.data.deinit(); }, + .decrqss => {}, } } @@ -142,6 +184,15 @@ pub const Command = union(enum) { return rem[0..idx]; } }; + + /// Supported DECRQSS settings + pub const DECRQSS = enum { + none, + sgr, + decscusr, + decstbm, + decslrm, + }; }; const State = union(enum) { @@ -152,8 +203,14 @@ const State = union(enum) { /// invalid due to some bad input, so we're ignoring the rest. ignore: void, - // XTGETTCAP + /// XTGETTCAP xtgettcap: std.ArrayList(u8), + + /// DECRQSS + decrqss: struct { + data: [2]u8 = undefined, + len: usize = 0, + }, }; test "unknown DCS command" { @@ -214,3 +271,39 @@ test "XTGETTCAP command invalid data" { try testing.expectEqualStrings("536D756C78", cmd.xtgettcap.next().?); 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); +} diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index dc6b7d6d3..5b515e04c 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -1485,6 +1485,65 @@ const StreamHandler = struct { 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); + }, } }