mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
@ -127,8 +127,8 @@ pub const Action = union(enum) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub const DCS = struct {
|
pub const DCS = struct {
|
||||||
intermediates: []u8,
|
intermediates: []const u8 = "",
|
||||||
params: []u16,
|
params: []const u16 = &.{},
|
||||||
final: u8,
|
final: u8,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
216
src/terminal/dcs.zig
Normal file
216
src/terminal/dcs.zig
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const terminal = @import("main.zig");
|
||||||
|
const DCS = terminal.DCS;
|
||||||
|
|
||||||
|
const log = std.log.scoped(.terminal_dcs);
|
||||||
|
|
||||||
|
/// DCS command handler. This should be hooked into a terminal.Stream handler.
|
||||||
|
/// The hook/put/unhook functions are meant to be called from the
|
||||||
|
/// terminal.stream dcsHook, dcsPut, and dcsUnhook functions, respectively.
|
||||||
|
pub const Handler = struct {
|
||||||
|
state: State = .{ .inactive = {} },
|
||||||
|
|
||||||
|
/// Maximum bytes any DCS command can take. This is to prevent
|
||||||
|
/// malicious input from causing us to allocate too much memory.
|
||||||
|
/// This is arbitrarily set to 1MB today, increase if needed.
|
||||||
|
max_bytes: usize = 1024 * 1024,
|
||||||
|
|
||||||
|
pub fn deinit(self: *Handler) void {
|
||||||
|
self.discard();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hook(self: *Handler, alloc: Allocator, dcs: DCS) void {
|
||||||
|
assert(self.state == .inactive);
|
||||||
|
self.state = if (tryHook(alloc, dcs)) |state_| state: {
|
||||||
|
if (state_) |state| break :state state else {
|
||||||
|
log.info("unknown DCS hook: {}", .{dcs});
|
||||||
|
break :state .{ .ignore = {} };
|
||||||
|
}
|
||||||
|
} else |err| state: {
|
||||||
|
log.info(
|
||||||
|
"error initializing DCS hook, will ignore hook err={}",
|
||||||
|
.{err},
|
||||||
|
);
|
||||||
|
break :state .{ .ignore = {} };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tryHook(alloc: Allocator, dcs: DCS) !?State {
|
||||||
|
return switch (dcs.intermediates.len) {
|
||||||
|
1 => switch (dcs.intermediates[0]) {
|
||||||
|
'+' => switch (dcs.final) {
|
||||||
|
// XTGETTCAP
|
||||||
|
// https://github.com/mitchellh/ghostty/issues/517
|
||||||
|
'q' => .{
|
||||||
|
.xtgettcap = try std.ArrayList(u8).initCapacity(
|
||||||
|
alloc,
|
||||||
|
128, // Arbitrary choice
|
||||||
|
),
|
||||||
|
},
|
||||||
|
|
||||||
|
else => null,
|
||||||
|
},
|
||||||
|
|
||||||
|
else => null,
|
||||||
|
},
|
||||||
|
|
||||||
|
else => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn put(self: *Handler, byte: u8) void {
|
||||||
|
self.tryPut(byte) catch |err| {
|
||||||
|
// On error we just discard our state and ignore the rest
|
||||||
|
log.info("error putting byte into DCS handler err={}", .{err});
|
||||||
|
self.discard();
|
||||||
|
self.state = .{ .ignore = {} };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tryPut(self: *Handler, byte: u8) !void {
|
||||||
|
switch (self.state) {
|
||||||
|
.inactive,
|
||||||
|
.ignore,
|
||||||
|
=> {},
|
||||||
|
|
||||||
|
.xtgettcap => |*list| {
|
||||||
|
if (list.items.len >= self.max_bytes) {
|
||||||
|
return error.OutOfMemory;
|
||||||
|
}
|
||||||
|
|
||||||
|
try list.append(byte);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unhook(self: *Handler) ?Command {
|
||||||
|
defer self.state = .{ .inactive = {} };
|
||||||
|
return switch (self.state) {
|
||||||
|
.inactive,
|
||||||
|
.ignore,
|
||||||
|
=> null,
|
||||||
|
|
||||||
|
.xtgettcap => |list| .{ .xtgettcap = .{ .data = list } },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn discard(self: *Handler) void {
|
||||||
|
switch (self.state) {
|
||||||
|
.inactive,
|
||||||
|
.ignore,
|
||||||
|
=> {},
|
||||||
|
|
||||||
|
.xtgettcap => |*list| list.deinit(),
|
||||||
|
}
|
||||||
|
|
||||||
|
self.state = .{ .inactive = {} };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Command = union(enum) {
|
||||||
|
/// XTGETTCAP
|
||||||
|
xtgettcap: XTGETTCAP,
|
||||||
|
|
||||||
|
pub fn deinit(self: Command) void {
|
||||||
|
switch (self) {
|
||||||
|
.xtgettcap => |*v| {
|
||||||
|
v.data.deinit();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const XTGETTCAP = struct {
|
||||||
|
data: std.ArrayList(u8),
|
||||||
|
i: usize = 0,
|
||||||
|
|
||||||
|
/// Returns the next terminfo key being requested and null
|
||||||
|
/// when there are no more keys. The returned value is NOT hex-decoded
|
||||||
|
/// because we expect to use a comptime lookup table.
|
||||||
|
pub fn next(self: *XTGETTCAP) ?[]const u8 {
|
||||||
|
if (self.i >= self.data.items.len) return null;
|
||||||
|
|
||||||
|
var rem = self.data.items[self.i..];
|
||||||
|
const idx = std.mem.indexOf(u8, rem, ";") orelse rem.len;
|
||||||
|
|
||||||
|
// Note that if we're at the end, idx + 1 is len + 1 so we're over
|
||||||
|
// the end but that's okay because our check above is >= so we'll
|
||||||
|
// never read.
|
||||||
|
self.i += idx + 1;
|
||||||
|
|
||||||
|
return rem[0..idx];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const State = union(enum) {
|
||||||
|
/// We're not in a DCS state at the moment.
|
||||||
|
inactive: void,
|
||||||
|
|
||||||
|
/// We're hooked, but its an unknown DCS command or one that went
|
||||||
|
/// invalid due to some bad input, so we're ignoring the rest.
|
||||||
|
ignore: void,
|
||||||
|
|
||||||
|
// XTGETTCAP
|
||||||
|
xtgettcap: std.ArrayList(u8),
|
||||||
|
};
|
||||||
|
|
||||||
|
test "unknown DCS command" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var h: Handler = .{};
|
||||||
|
defer h.deinit();
|
||||||
|
h.hook(alloc, .{ .final = 'A' });
|
||||||
|
try testing.expect(h.state == .ignore);
|
||||||
|
try testing.expect(h.unhook() == null);
|
||||||
|
try testing.expect(h.state == .inactive);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "XTGETTCAP command" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var h: Handler = .{};
|
||||||
|
defer h.deinit();
|
||||||
|
h.hook(alloc, .{ .intermediates = "+", .final = 'q' });
|
||||||
|
for ("536D756C78") |byte| h.put(byte);
|
||||||
|
var cmd = h.unhook().?;
|
||||||
|
defer cmd.deinit();
|
||||||
|
try testing.expect(cmd == .xtgettcap);
|
||||||
|
try testing.expectEqualStrings("536D756C78", cmd.xtgettcap.next().?);
|
||||||
|
try testing.expect(cmd.xtgettcap.next() == null);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "XTGETTCAP command multiple keys" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var h: Handler = .{};
|
||||||
|
defer h.deinit();
|
||||||
|
h.hook(alloc, .{ .intermediates = "+", .final = 'q' });
|
||||||
|
for ("536D756C78;536D756C78") |byte| h.put(byte);
|
||||||
|
var cmd = h.unhook().?;
|
||||||
|
defer cmd.deinit();
|
||||||
|
try testing.expect(cmd == .xtgettcap);
|
||||||
|
try testing.expectEqualStrings("536D756C78", cmd.xtgettcap.next().?);
|
||||||
|
try testing.expectEqualStrings("536D756C78", cmd.xtgettcap.next().?);
|
||||||
|
try testing.expect(cmd.xtgettcap.next() == null);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "XTGETTCAP command invalid data" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var h: Handler = .{};
|
||||||
|
defer h.deinit();
|
||||||
|
h.hook(alloc, .{ .intermediates = "+", .final = 'q' });
|
||||||
|
for ("who;536D756C78") |byte| h.put(byte);
|
||||||
|
var cmd = h.unhook().?;
|
||||||
|
defer cmd.deinit();
|
||||||
|
try testing.expect(cmd == .xtgettcap);
|
||||||
|
try testing.expectEqualStrings("who", cmd.xtgettcap.next().?);
|
||||||
|
try testing.expectEqualStrings("536D756C78", cmd.xtgettcap.next().?);
|
||||||
|
try testing.expect(cmd.xtgettcap.next() == null);
|
||||||
|
}
|
@ -6,6 +6,7 @@ const ansi = @import("ansi.zig");
|
|||||||
const csi = @import("csi.zig");
|
const csi = @import("csi.zig");
|
||||||
const sgr = @import("sgr.zig");
|
const sgr = @import("sgr.zig");
|
||||||
pub const apc = @import("apc.zig");
|
pub const apc = @import("apc.zig");
|
||||||
|
pub const dcs = @import("dcs.zig");
|
||||||
pub const osc = @import("osc.zig");
|
pub const osc = @import("osc.zig");
|
||||||
pub const point = @import("point.zig");
|
pub const point = @import("point.zig");
|
||||||
pub const color = @import("color.zig");
|
pub const color = @import("color.zig");
|
||||||
@ -16,6 +17,8 @@ pub const parse_table = @import("parse_table.zig");
|
|||||||
pub const Charset = charsets.Charset;
|
pub const Charset = charsets.Charset;
|
||||||
pub const CharsetSlot = charsets.Slots;
|
pub const CharsetSlot = charsets.Slots;
|
||||||
pub const CharsetActiveSlot = charsets.ActiveSlot;
|
pub const CharsetActiveSlot = charsets.ActiveSlot;
|
||||||
|
pub const CSI = Parser.Action.CSI;
|
||||||
|
pub const DCS = Parser.Action.DCS;
|
||||||
pub const MouseShape = @import("mouse_shape.zig").MouseShape;
|
pub const MouseShape = @import("mouse_shape.zig").MouseShape;
|
||||||
pub const Terminal = @import("Terminal.zig");
|
pub const Terminal = @import("Terminal.zig");
|
||||||
pub const Parser = @import("Parser.zig");
|
pub const Parser = @import("Parser.zig");
|
||||||
|
@ -64,9 +64,15 @@ pub fn Stream(comptime Handler: type) type {
|
|||||||
.csi_dispatch => |csi_action| try self.csiDispatch(csi_action),
|
.csi_dispatch => |csi_action| try self.csiDispatch(csi_action),
|
||||||
.esc_dispatch => |esc| try self.escDispatch(esc),
|
.esc_dispatch => |esc| try self.escDispatch(esc),
|
||||||
.osc_dispatch => |cmd| try self.oscDispatch(cmd),
|
.osc_dispatch => |cmd| try self.oscDispatch(cmd),
|
||||||
.dcs_hook => |dcs| log.warn("unhandled DCS hook: {}", .{dcs}),
|
.dcs_hook => |dcs| if (@hasDecl(T, "dcsHook")) {
|
||||||
.dcs_put => |code| log.warn("unhandled DCS put: {x}", .{code}),
|
try self.handler.dcsHook(dcs);
|
||||||
.dcs_unhook => log.warn("unhandled DCS unhook", .{}),
|
} else log.warn("unimplemented DCS hook", .{}),
|
||||||
|
.dcs_put => |code| if (@hasDecl(T, "dcsPut")) {
|
||||||
|
try self.handler.dcsPut(code);
|
||||||
|
} else log.warn("unimplemented DCS put: {x}", .{code}),
|
||||||
|
.dcs_unhook => if (@hasDecl(T, "dcsUnhook")) {
|
||||||
|
try self.handler.dcsUnhook();
|
||||||
|
} else log.warn("unimplemented DCS unhook", .{}),
|
||||||
.apc_start => if (@hasDecl(T, "apcStart")) {
|
.apc_start => if (@hasDecl(T, "apcStart")) {
|
||||||
try self.handler.apcStart();
|
try self.handler.apcStart();
|
||||||
} else log.warn("unimplemented APC start", .{}),
|
} else log.warn("unimplemented APC start", .{}),
|
||||||
|
@ -65,6 +65,87 @@ pub fn encode(self: Source, writer: anytype) !void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a ComptimeStringMap for all of the capabilities in this terminfo.
|
||||||
|
/// The value is the value that should be sent as a response to XTGETTCAP.
|
||||||
|
/// Important: the value is the FULL response included the escape sequences.
|
||||||
|
pub fn xtgettcapMap(comptime self: Source) type {
|
||||||
|
const KV = struct { []const u8, []const u8 };
|
||||||
|
|
||||||
|
// We have all of our capabilities plus To, TN, and RGB which aren't
|
||||||
|
// in the capabilities list but are query-able.
|
||||||
|
const len = self.capabilities.len + 3;
|
||||||
|
var kvs: [len]KV = .{.{ "", "" }} ** len;
|
||||||
|
|
||||||
|
// We first build all of our entries with raw K=V pairs.
|
||||||
|
kvs[0] = .{ "TN", self.names[0] };
|
||||||
|
kvs[1] = .{ "Co", "256" };
|
||||||
|
kvs[2] = .{ "RGB", "8" };
|
||||||
|
for (self.capabilities, 3..) |cap, i| {
|
||||||
|
kvs[i] = .{ cap.name, switch (cap.value) {
|
||||||
|
.canceled => @compileError("canceled not handled yet"),
|
||||||
|
.boolean => "",
|
||||||
|
.string => |v| v,
|
||||||
|
.numeric => |v| numeric: {
|
||||||
|
var buf: [10]u8 = undefined;
|
||||||
|
const num_len = std.fmt.formatIntBuf(&buf, v, 10, .upper, .{});
|
||||||
|
break :numeric buf[0..num_len];
|
||||||
|
},
|
||||||
|
} };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now go through and convert them all to hex-encoded strings.
|
||||||
|
for (&kvs) |*entry| {
|
||||||
|
// The key is just the raw hex-encoded string
|
||||||
|
entry[0] = hexencode(entry[0]);
|
||||||
|
|
||||||
|
// The value is more complex
|
||||||
|
var buf: [5 + entry[0].len + 1 + (entry[1].len * 2) + 2]u8 = undefined;
|
||||||
|
entry[1] = if (std.mem.eql(u8, entry[1], "")) std.fmt.bufPrint(
|
||||||
|
&buf,
|
||||||
|
"\x1bP1+r{s}\x1b\\",
|
||||||
|
.{entry[0]}, // important: hex-encoded name
|
||||||
|
) catch unreachable else std.fmt.bufPrint(
|
||||||
|
&buf,
|
||||||
|
"\x1bP1+r{s}={s}\x1b\\",
|
||||||
|
.{ entry[0], hexencode(entry[1]) }, // important: hex-encoded name
|
||||||
|
) catch unreachable;
|
||||||
|
}
|
||||||
|
|
||||||
|
return std.ComptimeStringMap([]const u8, kvs);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hexencode(comptime input: []const u8) []const u8 {
|
||||||
|
return comptime &(std.fmt.bytesToHex(input, .upper));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "xtgettcap map" {
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
const src: Source = .{
|
||||||
|
.names = &.{
|
||||||
|
"ghostty",
|
||||||
|
"xterm-ghostty",
|
||||||
|
"Ghostty",
|
||||||
|
},
|
||||||
|
|
||||||
|
.capabilities = &.{
|
||||||
|
.{ .name = "am", .value = .{ .boolean = {} } },
|
||||||
|
.{ .name = "colors", .value = .{ .numeric = 256 } },
|
||||||
|
.{ .name = "Smulx", .value = .{ .string = "\\E[4:%p1%dm" } },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const map = comptime src.xtgettcapMap();
|
||||||
|
try testing.expectEqualStrings(
|
||||||
|
"\x1bP1+r616D\x1b\\",
|
||||||
|
map.get(hexencode("am")).?,
|
||||||
|
);
|
||||||
|
try testing.expectEqualStrings(
|
||||||
|
"\x1bP1+r536D756C78=5C455B343A25703125646D\x1b\\",
|
||||||
|
map.get(hexencode("Smulx")).?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
test "encode" {
|
test "encode" {
|
||||||
const src: Source = .{
|
const src: Source = .{
|
||||||
.names = &.{
|
.names = &.{
|
||||||
|
@ -13,6 +13,7 @@ const Command = @import("../Command.zig");
|
|||||||
const Pty = @import("../Pty.zig");
|
const Pty = @import("../Pty.zig");
|
||||||
const SegmentedPool = @import("../segmented_pool.zig").SegmentedPool;
|
const SegmentedPool = @import("../segmented_pool.zig").SegmentedPool;
|
||||||
const terminal = @import("../terminal/main.zig");
|
const terminal = @import("../terminal/main.zig");
|
||||||
|
const terminfo = @import("../terminfo/main.zig");
|
||||||
const xev = @import("xev");
|
const xev = @import("xev");
|
||||||
const renderer = @import("../renderer.zig");
|
const renderer = @import("../renderer.zig");
|
||||||
const tracy = @import("tracy");
|
const tracy = @import("tracy");
|
||||||
@ -1157,6 +1158,11 @@ const StreamHandler = struct {
|
|||||||
/// the kitty graphics protocol.
|
/// the kitty graphics protocol.
|
||||||
apc: terminal.apc.Handler = .{},
|
apc: terminal.apc.Handler = .{},
|
||||||
|
|
||||||
|
/// The DCS handler maintains DCS state. DCS is like CSI or OSC,
|
||||||
|
/// but requires more stateful parsing. This is used by functionality
|
||||||
|
/// such as XTGETTCAP.
|
||||||
|
dcs: terminal.dcs.Handler = .{},
|
||||||
|
|
||||||
/// This is set to true when a message was written to the writer
|
/// This is set to true when a message was written to the writer
|
||||||
/// mailbox. This can be used by callers to determine if they need
|
/// mailbox. This can be used by callers to determine if they need
|
||||||
/// to wake up the writer.
|
/// to wake up the writer.
|
||||||
@ -1173,6 +1179,7 @@ const StreamHandler = struct {
|
|||||||
|
|
||||||
pub fn deinit(self: *StreamHandler) void {
|
pub fn deinit(self: *StreamHandler) void {
|
||||||
self.apc.deinit();
|
self.apc.deinit();
|
||||||
|
self.dcs.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fn queueRender(self: *StreamHandler) !void {
|
inline fn queueRender(self: *StreamHandler) !void {
|
||||||
@ -1199,6 +1206,30 @@ const StreamHandler = struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn dcsHook(self: *StreamHandler, dcs: terminal.DCS) !void {
|
||||||
|
self.dcs.hook(self.alloc, dcs);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dcsPut(self: *StreamHandler, byte: u8) !void {
|
||||||
|
self.dcs.put(byte);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dcsUnhook(self: *StreamHandler) !void {
|
||||||
|
var cmd = self.dcs.unhook() orelse return;
|
||||||
|
defer cmd.deinit();
|
||||||
|
|
||||||
|
// log.warn("DCS command: {}", .{cmd});
|
||||||
|
switch (cmd) {
|
||||||
|
.xtgettcap => |*gettcap| {
|
||||||
|
const map = comptime terminfo.ghostty.xtgettcapMap();
|
||||||
|
while (gettcap.next()) |key| {
|
||||||
|
const response = map.get(key) orelse continue;
|
||||||
|
self.messageWriter(.{ .write_stable = response });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn apcStart(self: *StreamHandler) !void {
|
pub fn apcStart(self: *StreamHandler) !void {
|
||||||
self.apc.start();
|
self.apc.start();
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user