mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 07:46:12 +03:00
terminal: DCS handler, XTGETTCAP parsing
This commit is contained in:
@ -127,8 +127,8 @@ pub const Action = union(enum) {
|
||||
};
|
||||
|
||||
pub const DCS = struct {
|
||||
intermediates: []u8,
|
||||
params: []u16,
|
||||
intermediates: []const u8 = "",
|
||||
params: []const u16 = &.{},
|
||||
final: u8,
|
||||
};
|
||||
|
||||
|
218
src/terminal/dcs.zig
Normal file
218
src/terminal/dcs.zig
Normal file
@ -0,0 +1,218 @@
|
||||
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),
|
||||
|
||||
// Note: do not reset this. The next function mutates data so it is
|
||||
// unsafe to reset this and reiterate.
|
||||
i: usize = 0,
|
||||
|
||||
/// Returns the next terminfo key being requested and null
|
||||
/// when there are no more keys.
|
||||
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;
|
||||
|
||||
// HEX decode in-place so we don't have to allocate. If invalid
|
||||
// hex is given then we just return null and stop processing.
|
||||
return std.fmt.hexToBytes(rem, rem[0..idx]) catch null;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
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("Smulx", 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("Smulx", cmd.xtgettcap.next().?);
|
||||
try testing.expectEqualStrings("Smulx", 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.expect(cmd.xtgettcap.next() == null);
|
||||
}
|
@ -6,6 +6,7 @@ const ansi = @import("ansi.zig");
|
||||
const csi = @import("csi.zig");
|
||||
const sgr = @import("sgr.zig");
|
||||
pub const apc = @import("apc.zig");
|
||||
pub const dcs = @import("dcs.zig");
|
||||
pub const osc = @import("osc.zig");
|
||||
pub const point = @import("point.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 CharsetSlot = charsets.Slots;
|
||||
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 Terminal = @import("Terminal.zig");
|
||||
pub const Parser = @import("Parser.zig");
|
||||
|
@ -64,7 +64,9 @@ pub fn Stream(comptime Handler: type) type {
|
||||
.csi_dispatch => |csi_action| try self.csiDispatch(csi_action),
|
||||
.esc_dispatch => |esc| try self.escDispatch(esc),
|
||||
.osc_dispatch => |cmd| try self.oscDispatch(cmd),
|
||||
.dcs_hook => |dcs| log.warn("unhandled DCS hook: {}", .{dcs}),
|
||||
.dcs_hook => |dcs| if (@hasDecl(T, "dcsHook")) {
|
||||
try self.handler.dcsHook(dcs);
|
||||
} else log.warn("unimplemented DCS hook", .{}),
|
||||
.dcs_put => |code| log.warn("unhandled DCS put: {x}", .{code}),
|
||||
.dcs_unhook => log.warn("unhandled DCS unhook", .{}),
|
||||
.apc_start => if (@hasDecl(T, "apcStart")) {
|
||||
|
@ -1199,6 +1199,11 @@ const StreamHandler = struct {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn dcsHook(self: *StreamHandler, dcs: terminal.DCS) !void {
|
||||
_ = self;
|
||||
log.warn("DCS HOOK: {}", .{dcs});
|
||||
}
|
||||
|
||||
pub fn apcStart(self: *StreamHandler) !void {
|
||||
self.apc.start();
|
||||
}
|
||||
|
Reference in New Issue
Block a user