mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
Merge pull request #1094 from gpanders/decrqss
termio: implement DECRQSS
This commit is contained in:
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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: u2 = 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);
|
||||
}
|
||||
|
@ -1524,6 +1524,72 @@ 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();
|
||||
|
||||
// Offset the stream position to just past the response prefix.
|
||||
// We will write the "payload" (if any) below. If no payload is
|
||||
// written then we send an invalid DECRPSS response.
|
||||
const prefix_fmt = "\x1bP{d}$r";
|
||||
const prefix_len = std.fmt.comptimePrint(prefix_fmt, .{0}).len;
|
||||
stream.pos = prefix_len;
|
||||
|
||||
switch (decrqss) {
|
||||
// Invalid or unhandled request
|
||||
.none => {},
|
||||
|
||||
.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 => {
|
||||
// We only send a valid response when left and right
|
||||
// margin mode (DECLRMM) is enabled.
|
||||
if (self.terminal.modes.get(.enable_left_and_right_margin)) {
|
||||
try writer.print("{d};{d}s", .{
|
||||
self.terminal.scrolling_region.left + 1,
|
||||
self.terminal.scrolling_region.right + 1,
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// Our response is valid if we have a response payload
|
||||
const valid = stream.pos > prefix_len;
|
||||
|
||||
// Write the terminator
|
||||
try writer.writeAll("\x1b\\");
|
||||
|
||||
// Write the response prefix into the buffer
|
||||
_ = try std.fmt.bufPrint(response[0..prefix_len], prefix_fmt, .{@intFromBool(valid)});
|
||||
const msg = try termio.Message.writeReq(self.alloc, response[0..stream.pos]);
|
||||
self.messageWriter(msg);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user