mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-24 16:11:43 +00:00
terminal/apc: support reporting unknown APC sequences
This commit is contained in:
@@ -52,10 +52,10 @@ const Handler = struct {
|
||||
.apc_start => self.apc.start(),
|
||||
.apc_put => self.apc.feed(self.alloc, value),
|
||||
.apc_put_slice => self.apc.feedSlice(self.alloc, value.bytes),
|
||||
.apc_end => if (self.apc.end()) |cmd| {
|
||||
var c = cmd;
|
||||
std.mem.doNotOptimizeAway(&c);
|
||||
c.deinit(self.alloc);
|
||||
.apc_end => if (self.apc.end()) |result| {
|
||||
var r = result;
|
||||
std.mem.doNotOptimizeAway(&r);
|
||||
r.deinit(self.alloc);
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@ pub const Terminal = terminal.Terminal;
|
||||
pub const TerminalStream = terminal.TerminalStream;
|
||||
pub const Stream = terminal.Stream;
|
||||
pub const StreamAction = terminal.StreamAction;
|
||||
pub const UnknownSequence = terminal.UnknownSequence;
|
||||
pub const Cursor = Screen.Cursor;
|
||||
pub const CursorStyle = Screen.CursorStyle;
|
||||
pub const CursorStyleReq = terminal.CursorStyle;
|
||||
|
||||
@@ -13,6 +13,10 @@ const log = std.log.scoped(.terminal_apc);
|
||||
pub const Handler = struct {
|
||||
state: State = .inactive,
|
||||
|
||||
/// Maximum content bytes retained for unsupported APC identifiers. Zero
|
||||
/// drops and ignores unknown APC values.
|
||||
unknown_max_bytes: usize = 0,
|
||||
|
||||
/// Maximum bytes each APC protocol can buffer. This is to prevent
|
||||
/// malicious input from causing us to allocate too much memory.
|
||||
/// If you want to be lazy and set a single value for all protocols,
|
||||
@@ -23,8 +27,7 @@ pub const Handler = struct {
|
||||
}),
|
||||
|
||||
/// Protocols recognized by this APC handler. When a protocol is absent,
|
||||
/// matching APC sequences are ignored so callers see the same behavior as
|
||||
/// an unsupported protocol: no command execution and no response.
|
||||
/// matching APC sequences are ignored and are not reported as unknown.
|
||||
enabled: std.EnumSet(Protocol) = .initFull(),
|
||||
|
||||
pub fn deinit(self: *Handler) void {
|
||||
@@ -50,51 +53,70 @@ pub const Handler = struct {
|
||||
// recognize it so there is no need to store the data in memory.
|
||||
.ignore => return,
|
||||
|
||||
// Unsupported APC content is retained only when enabled.
|
||||
.unknown => |*unknown| unknown.append(&.{byte}),
|
||||
|
||||
// We identify the APC command by the first byte.
|
||||
.identify => |*id| id: {
|
||||
// Kitty graphics is detected immediately on the `G` byte,
|
||||
// since commands begin immediately after with no termination
|
||||
// character after the 'G'.
|
||||
if (comptime build_options.kitty_graphics) {
|
||||
if (id.len == 0 and
|
||||
byte == 'G' and
|
||||
self.enabled.contains(.kitty))
|
||||
{
|
||||
self.state = .{ .kitty = .init(
|
||||
alloc,
|
||||
self.max_bytes.get(.kitty) orelse
|
||||
Protocol.defaultMaxBytes(.kitty),
|
||||
) };
|
||||
break :id;
|
||||
if (id.len == 0 and byte == 'G') {
|
||||
if (comptime build_options.kitty_graphics) {
|
||||
if (self.enabled.contains(.kitty)) {
|
||||
self.state = .{ .kitty = .init(
|
||||
alloc,
|
||||
self.max_bytes.get(.kitty) orelse
|
||||
Protocol.defaultMaxBytes(.kitty),
|
||||
) };
|
||||
} else {
|
||||
self.state = .ignore;
|
||||
}
|
||||
} else {
|
||||
self.state = .ignore;
|
||||
}
|
||||
break :id;
|
||||
}
|
||||
|
||||
// If we hit `;` then identify...
|
||||
if (byte == ';') {
|
||||
const str = id.buf[0..id.len];
|
||||
if (std.mem.eql(u8, str, "25a1") and
|
||||
self.enabled.contains(.glyph))
|
||||
{
|
||||
self.state = .{ .glyph = .init(
|
||||
alloc,
|
||||
self.max_bytes.get(.glyph) orelse
|
||||
Protocol.defaultMaxBytes(.glyph),
|
||||
) };
|
||||
if (std.mem.eql(u8, str, glyph.identifier)) {
|
||||
if (self.enabled.contains(.glyph)) {
|
||||
self.state = .{ .glyph = .init(
|
||||
alloc,
|
||||
self.max_bytes.get(.glyph) orelse
|
||||
Protocol.defaultMaxBytes(.glyph),
|
||||
) };
|
||||
} else {
|
||||
self.state = .ignore;
|
||||
}
|
||||
} else {
|
||||
self.state = .ignore;
|
||||
self.beginUnknown(alloc, str, &.{byte});
|
||||
}
|
||||
|
||||
break :id;
|
||||
}
|
||||
|
||||
// If we're out of space to buffer then we're done.
|
||||
// If we're out of identification space, the identifier is
|
||||
// unsupported. Preserve the buffered prefix before replacing
|
||||
// the identify union state.
|
||||
if (id.len >= id.buf.len) {
|
||||
self.state = .ignore;
|
||||
self.beginUnknown(alloc, id.buf[0..id.len], &.{byte});
|
||||
break :id;
|
||||
}
|
||||
|
||||
const expected_idx: usize = id.len;
|
||||
id.buf[id.len] = byte;
|
||||
id.len += 1;
|
||||
|
||||
// Once the buffered input is no longer a prefix of a known
|
||||
// protocol, it is an unsupported identifier.
|
||||
if (self.unknown_max_bytes > 0 and
|
||||
byte != glyph.identifier[expected_idx])
|
||||
{
|
||||
self.beginUnknown(alloc, id.buf[0..id.len], &.{});
|
||||
}
|
||||
},
|
||||
|
||||
.kitty => |*p| if (comptime build_options.kitty_graphics) {
|
||||
@@ -113,6 +135,27 @@ pub const Handler = struct {
|
||||
}
|
||||
}
|
||||
|
||||
/// Transition from protocol identification to bounded unknown capture.
|
||||
fn beginUnknown(
|
||||
self: *Handler,
|
||||
alloc: Allocator,
|
||||
prefix: []const u8,
|
||||
suffix: []const u8,
|
||||
) void {
|
||||
const max_bytes = self.unknown_max_bytes;
|
||||
if (max_bytes == 0) {
|
||||
self.state = .ignore;
|
||||
return;
|
||||
}
|
||||
|
||||
// Build the replacement before overwriting identify because prefix
|
||||
// points into that union field.
|
||||
var unknown: UnknownBuilder = .init(alloc, max_bytes);
|
||||
unknown.append(prefix);
|
||||
unknown.append(suffix);
|
||||
self.state = .{ .unknown = unknown };
|
||||
}
|
||||
|
||||
/// Feed a slice of bytes to the handler. This is equivalent to
|
||||
/// calling feed for each byte in order, but protocol payload bytes
|
||||
/// are passed through in bulk so large payloads (e.g. Kitty graphics
|
||||
@@ -126,6 +169,12 @@ pub const Handler = struct {
|
||||
// We're ignoring this APC command; drop the whole slice.
|
||||
.ignore => return,
|
||||
|
||||
// We're capturing an unknown APC command, so store it.
|
||||
.unknown => |*unknown| {
|
||||
unknown.append(rem);
|
||||
return;
|
||||
},
|
||||
|
||||
// Identification consumes at most a few bytes; step
|
||||
// through them one at a time until the state changes.
|
||||
.identify => {
|
||||
@@ -154,6 +203,8 @@ pub const Handler = struct {
|
||||
}
|
||||
}
|
||||
|
||||
/// Complete the current APC. The caller owns a returned result and must
|
||||
/// call `Command.deinit` with the allocator used while feeding the APC.
|
||||
pub fn end(self: *Handler) ?Command {
|
||||
defer {
|
||||
self.state.deinit();
|
||||
@@ -163,6 +214,7 @@ pub const Handler = struct {
|
||||
return switch (self.state) {
|
||||
.inactive => unreachable,
|
||||
.ignore, .identify => null,
|
||||
.unknown => |*unknown| .{ .unknown = unknown.toOwned() },
|
||||
.kitty => |*p| kitty: {
|
||||
if (comptime !build_options.kitty_graphics) unreachable;
|
||||
|
||||
@@ -207,7 +259,7 @@ pub const State = union(enum) {
|
||||
///
|
||||
identify: struct {
|
||||
len: u3 = 0,
|
||||
buf: [4]u8 = undefined,
|
||||
buf: [glyph.identifier.len]u8 = undefined,
|
||||
},
|
||||
|
||||
/// Kitty graphics protocol
|
||||
@@ -219,9 +271,15 @@ pub const State = union(enum) {
|
||||
/// Glyph protocol
|
||||
glyph: glyph.CommandParser,
|
||||
|
||||
/// An unsupported APC retained for the optional unknown callback.
|
||||
/// Keep this after recognized protocol states so their tag values and
|
||||
/// generated dispatch stay stable when unknown capture is unused.
|
||||
unknown: UnknownBuilder,
|
||||
|
||||
pub fn deinit(self: *State) void {
|
||||
switch (self.*) {
|
||||
.inactive, .ignore, .identify => {},
|
||||
.unknown => |*v| v.deinit(),
|
||||
.glyph => |*v| v.deinit(),
|
||||
.kitty => |*v| if (comptime build_options.kitty_graphics)
|
||||
v.deinit()
|
||||
@@ -231,6 +289,95 @@ pub const State = union(enum) {
|
||||
}
|
||||
};
|
||||
|
||||
/// UnknownBuilder is responsible for accumulating bytes for an
|
||||
/// unidentified APC command if unknown capture is enabled.
|
||||
const UnknownBuilder = struct {
|
||||
data: std.ArrayList(u8) = .empty,
|
||||
alloc: Allocator,
|
||||
max_bytes: usize,
|
||||
truncated: bool = false,
|
||||
|
||||
fn init(alloc: Allocator, max_bytes: usize) UnknownBuilder {
|
||||
return .{
|
||||
.alloc = alloc,
|
||||
.max_bytes = max_bytes,
|
||||
};
|
||||
}
|
||||
|
||||
fn deinit(self: *UnknownBuilder) void {
|
||||
self.data.deinit(self.alloc);
|
||||
self.data = .empty;
|
||||
}
|
||||
|
||||
// Append some bytes to the unknown capture. This flags as truncated
|
||||
// if allocation fails or we reach our byte limit, therefore
|
||||
// it can't fail.
|
||||
fn append(self: *UnknownBuilder, bytes: []const u8) void {
|
||||
if (bytes.len == 0) return;
|
||||
const current = self.data.items.len;
|
||||
|
||||
// Determine how many bytes we can store in this append and
|
||||
// if it is less than our input, then we have to note we're
|
||||
// truncating.
|
||||
const retained = @min(bytes.len, self.max_bytes -| current);
|
||||
if (retained < bytes.len) self.truncated = true;
|
||||
|
||||
// If we require more bytes than our capacity allows then we
|
||||
// need to grow.
|
||||
const required = current + retained;
|
||||
if (required > self.data.capacity) {
|
||||
const capacity = @min(
|
||||
self.max_bytes,
|
||||
@max(required, @max(self.data.capacity *| 2, 1)),
|
||||
);
|
||||
self.data.ensureTotalCapacityPrecise(
|
||||
self.alloc,
|
||||
capacity,
|
||||
) catch {
|
||||
self.truncated = true;
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
self.data.appendSliceAssumeCapacity(bytes[0..retained]);
|
||||
}
|
||||
|
||||
/// Convert the current capture state to an Unknown where allocator
|
||||
/// ownership shifts to Unknown. Removes any accumulated unknown
|
||||
/// capture in this struct.
|
||||
///
|
||||
/// This can't fail because if there is an allocator issue we return
|
||||
/// an empty truncate-flagged Unknown.
|
||||
fn toOwned(self: *UnknownBuilder) Unknown {
|
||||
// toOwnedSlice allows us to reuse data but this makes error
|
||||
// handling a little simpler.
|
||||
const content = self.data.toOwnedSlice(self.alloc) catch {
|
||||
self.data.deinit(self.alloc);
|
||||
self.data = .empty;
|
||||
return .{
|
||||
.content = self.data.items,
|
||||
.truncated = true,
|
||||
};
|
||||
};
|
||||
|
||||
return .{
|
||||
.content = content,
|
||||
.truncated = self.truncated,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// An unsupported APC returned by `Handler.end`.
|
||||
pub const Unknown = struct {
|
||||
content: []u8,
|
||||
truncated: bool,
|
||||
|
||||
pub fn deinit(self: *Unknown, alloc: Allocator) void {
|
||||
if (self.content.len > 0) alloc.free(self.content);
|
||||
self.* = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
/// Possible APC command types.
|
||||
pub const Protocol = enum {
|
||||
kitty,
|
||||
@@ -261,14 +408,15 @@ pub const Protocol = enum {
|
||||
}
|
||||
};
|
||||
|
||||
/// Possible APC commands.
|
||||
pub const Command = union(Protocol) {
|
||||
/// A recognized or unsupported APC command.
|
||||
pub const Command = union(enum) {
|
||||
kitty: if (build_options.kitty_graphics)
|
||||
kitty_gfx.Command
|
||||
else
|
||||
void,
|
||||
|
||||
glyph: glyph.Request,
|
||||
unknown: Unknown,
|
||||
|
||||
pub fn deinit(self: *Command, alloc: Allocator) void {
|
||||
switch (self.*) {
|
||||
@@ -278,6 +426,7 @@ pub const Command = union(Protocol) {
|
||||
unreachable,
|
||||
|
||||
.glyph => |*v| v.deinit(alloc),
|
||||
.unknown => |*v| v.deinit(alloc),
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -292,6 +441,64 @@ test "unknown APC command" {
|
||||
try testing.expect(h.end() == null);
|
||||
}
|
||||
|
||||
test "capture unknown APC command" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var h: Handler = .{ .unknown_max_bytes = 5 };
|
||||
defer h.deinit();
|
||||
h.start();
|
||||
h.feedSlice(alloc, "abcd;payload");
|
||||
|
||||
var result = h.end().?;
|
||||
defer result.deinit(alloc);
|
||||
const unknown = &result.unknown;
|
||||
try testing.expectEqualStrings("abcd;", unknown.content);
|
||||
try testing.expect(unknown.truncated);
|
||||
}
|
||||
|
||||
test "capture short unknown APC command" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var h: Handler = .{ .unknown_max_bytes = 16 };
|
||||
defer h.deinit();
|
||||
h.start();
|
||||
h.feed(alloc, 'X');
|
||||
|
||||
var result = h.end().?;
|
||||
const unknown = &result.unknown;
|
||||
try testing.expectEqualStrings("X", unknown.content);
|
||||
try testing.expect(!unknown.truncated);
|
||||
result.deinit(alloc);
|
||||
|
||||
h.unknown_max_bytes = 1;
|
||||
h.start();
|
||||
h.feedSlice(alloc, "XYZ");
|
||||
result = h.end().?;
|
||||
const truncated = &result.unknown;
|
||||
try testing.expectEqualStrings("X", truncated.content);
|
||||
try testing.expect(truncated.truncated);
|
||||
result.deinit(alloc);
|
||||
}
|
||||
|
||||
test "disabled known APC protocol is not unknown" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var h: Handler = .{ .unknown_max_bytes = 64 };
|
||||
defer h.deinit();
|
||||
h.enable(.glyph, false);
|
||||
h.start();
|
||||
h.feedSlice(alloc, "25a1;q;cp=E0A0");
|
||||
try testing.expect(h.end() == null);
|
||||
|
||||
// An incomplete known protocol identifier is malformed, not unknown.
|
||||
h.start();
|
||||
h.feedSlice(alloc, "25a");
|
||||
try testing.expect(h.end() == null);
|
||||
}
|
||||
|
||||
test "garbage Kitty command" {
|
||||
if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
|
||||
|
||||
@@ -374,9 +581,9 @@ test "valid Kitty command" {
|
||||
const input = "Gf=24,s=10,v=20,hello=world";
|
||||
for (input) |c| h.feed(alloc, c);
|
||||
|
||||
var cmd = h.end().?;
|
||||
defer cmd.deinit(alloc);
|
||||
try testing.expect(cmd == .kitty);
|
||||
var result = h.end().?;
|
||||
defer result.deinit(alloc);
|
||||
try testing.expect(result == .kitty);
|
||||
}
|
||||
|
||||
test "identify with unrecognized command" {
|
||||
@@ -436,10 +643,10 @@ test "valid glyph command" {
|
||||
h.start();
|
||||
for ("25a1;q;cp=E0A0") |c| h.feed(alloc, c);
|
||||
|
||||
var cmd = h.end().?;
|
||||
defer cmd.deinit(alloc);
|
||||
try testing.expect(cmd == .glyph);
|
||||
try testing.expect(cmd.glyph == .query);
|
||||
var result = h.end().?;
|
||||
defer result.deinit(alloc);
|
||||
try testing.expect(result == .glyph);
|
||||
try testing.expect(result.glyph == .query);
|
||||
}
|
||||
|
||||
test "feedSlice valid Kitty command" {
|
||||
@@ -452,12 +659,12 @@ test "feedSlice valid Kitty command" {
|
||||
h.start();
|
||||
h.feedSlice(alloc, "Gf=24,s=10,v=20;aGVsbG8=");
|
||||
|
||||
var cmd = h.end().?;
|
||||
defer cmd.deinit(alloc);
|
||||
try testing.expect(cmd == .kitty);
|
||||
var result = h.end().?;
|
||||
defer result.deinit(alloc);
|
||||
try testing.expect(result == .kitty);
|
||||
|
||||
// The payload is base64-decoded by the parser on completion.
|
||||
try testing.expectEqualStrings("hello", cmd.kitty.data);
|
||||
try testing.expectEqualStrings("hello", result.kitty.data);
|
||||
}
|
||||
|
||||
test "feedSlice identify split across slices" {
|
||||
@@ -472,12 +679,12 @@ test "feedSlice identify split across slices" {
|
||||
h.feedSlice(alloc, "f=24,s=10,");
|
||||
h.feedSlice(alloc, "v=20;aGVsbG8=");
|
||||
|
||||
var cmd = h.end().?;
|
||||
defer cmd.deinit(alloc);
|
||||
try testing.expect(cmd == .kitty);
|
||||
var result = h.end().?;
|
||||
defer result.deinit(alloc);
|
||||
try testing.expect(result == .kitty);
|
||||
|
||||
// The payload is base64-decoded by the parser on completion.
|
||||
try testing.expectEqualStrings("hello", cmd.kitty.data);
|
||||
try testing.expectEqualStrings("hello", result.kitty.data);
|
||||
}
|
||||
|
||||
test "feedSlice unknown APC command is ignored" {
|
||||
@@ -500,10 +707,10 @@ test "feedSlice valid glyph command" {
|
||||
h.start();
|
||||
h.feedSlice(alloc, "25a1;q;cp=E0A0");
|
||||
|
||||
var cmd = h.end().?;
|
||||
defer cmd.deinit(alloc);
|
||||
try testing.expect(cmd == .glyph);
|
||||
try testing.expect(cmd.glyph == .query);
|
||||
var result = h.end().?;
|
||||
defer result.deinit(alloc);
|
||||
try testing.expect(result == .glyph);
|
||||
try testing.expect(result.glyph == .query);
|
||||
}
|
||||
|
||||
test "feedSlice kitty max bytes exceeded" {
|
||||
|
||||
@@ -150,6 +150,9 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
/// APC identifier for the glyph protocol.
|
||||
pub const identifier = "25a1";
|
||||
|
||||
pub const request = @import("glyph/request.zig");
|
||||
pub const response = @import("glyph/response.zig");
|
||||
pub const execute = @import("glyph/execute.zig").execute;
|
||||
|
||||
@@ -59,6 +59,7 @@ pub const Terminal = @import("Terminal.zig");
|
||||
pub const TerminalStream = stream_terminal.Stream;
|
||||
pub const Stream = stream.Stream;
|
||||
pub const StreamAction = stream.Action;
|
||||
pub const UnknownSequence = stream_terminal.Handler.UnknownSequence;
|
||||
pub const Cursor = Screen.Cursor;
|
||||
pub const CursorStyle = Screen.CursorStyle;
|
||||
pub const CursorStyleReq = ansi.CursorStyle;
|
||||
|
||||
@@ -110,7 +110,7 @@ pub const Action = union(Key) {
|
||||
dcs_put: u8,
|
||||
dcs_unhook,
|
||||
apc_start,
|
||||
apc_end,
|
||||
apc_end: ApcEnd,
|
||||
apc_put: u8,
|
||||
apc_put_slice: ApcPutSlice,
|
||||
end_hyperlink,
|
||||
@@ -291,6 +291,11 @@ pub const Action = union(Key) {
|
||||
}
|
||||
};
|
||||
|
||||
pub const ApcEnd = extern struct {
|
||||
/// False when CAN, SUB, or another aborting transition ended the APC.
|
||||
terminated: bool,
|
||||
};
|
||||
|
||||
pub const InvokeCharset = lib.Struct(lib.target, struct {
|
||||
bank: charsets.ActiveSlot,
|
||||
charset: charsets.Slots,
|
||||
@@ -774,18 +779,21 @@ pub fn Stream(comptime H: type) type {
|
||||
std.ascii.control_code.esc => {
|
||||
self.parser.clear();
|
||||
self.parser.state = .escape;
|
||||
self.handler.vt(.apc_end, {});
|
||||
self.handler.vt(.apc_end, .{ .terminated = true });
|
||||
offset += 1;
|
||||
continue;
|
||||
},
|
||||
0x9C => {
|
||||
self.parser.state = .ground;
|
||||
self.handler.vt(.apc_end, {});
|
||||
self.handler.vt(.apc_end, .{ .terminated = true });
|
||||
offset += 1;
|
||||
continue;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
|
||||
// Aborting transitions need the scalar path so the
|
||||
// handler can distinguish them from terminators.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1127,7 +1135,9 @@ pub fn Stream(comptime H: type) type {
|
||||
.dcs_unhook => self.handler.vt(.dcs_unhook, {}),
|
||||
.apc_start => self.handler.vt(.apc_start, {}),
|
||||
.apc_put => |code| self.handler.vt(.apc_put, code),
|
||||
.apc_end => self.handler.vt(.apc_end, {}),
|
||||
.apc_end => self.handler.vt(.apc_end, .{
|
||||
.terminated = c == std.ascii.control_code.esc or c == 0x9C,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,12 @@ pub const Handler = struct {
|
||||
/// The DCS command handler maintains state for DCS queries.
|
||||
dcs_handler: dcs.Handler = .{},
|
||||
|
||||
/// Called for sequence identifiers not supported by this library.
|
||||
/// Currently, only APC is reported. Content is borrowed and only valid
|
||||
/// for the duration of the callback. Set `apc_handler.unknown_max_bytes`
|
||||
/// before starting the Stream to enable APC capture.
|
||||
unknown_sequence: ?*const fn (*Handler, UnknownSequence) void = null,
|
||||
|
||||
pub const Effects = struct {
|
||||
/// Called when the terminal needs to write data back to the pty,
|
||||
/// e.g. in response to a DECRQM query. The data is only valid
|
||||
@@ -157,6 +163,18 @@ pub const Handler = struct {
|
||||
};
|
||||
};
|
||||
|
||||
/// A sequence unsupported by the active handler. Payload data is borrowed
|
||||
/// only for the duration of the handler callback.
|
||||
pub const UnknownSequence = union(enum) {
|
||||
apc: String,
|
||||
|
||||
/// Content between a string sequence's introducer and terminator.
|
||||
pub const String = struct {
|
||||
content: []const u8,
|
||||
truncated: bool,
|
||||
};
|
||||
};
|
||||
|
||||
pub fn init(terminal: *Terminal) Handler {
|
||||
return .{
|
||||
.terminal = terminal,
|
||||
@@ -211,6 +229,11 @@ pub const Handler = struct {
|
||||
};
|
||||
}
|
||||
|
||||
fn unknownSequence(self: *Handler, value: UnknownSequence) void {
|
||||
const func = self.unknown_sequence orelse return;
|
||||
func(self, value);
|
||||
}
|
||||
|
||||
inline fn vtFallible(
|
||||
self: *Handler,
|
||||
comptime action: Action.Tag,
|
||||
@@ -326,7 +349,7 @@ pub const Handler = struct {
|
||||
.apc_start => self.apc_handler.start(),
|
||||
.apc_put => self.apc_handler.feed(self.terminal.gpa(), value),
|
||||
.apc_put_slice => self.apc_handler.feedSlice(self.terminal.gpa(), value.bytes),
|
||||
.apc_end => self.apcEnd(),
|
||||
.apc_end => self.apcEnd(value.terminated),
|
||||
|
||||
// Effect-based handlers
|
||||
.bell => self.bell(),
|
||||
@@ -948,13 +971,18 @@ pub const Handler = struct {
|
||||
}
|
||||
}
|
||||
|
||||
fn apcEnd(self: *Handler) void {
|
||||
fn apcEnd(self: *Handler, terminated: bool) void {
|
||||
const io = self.terminal.io();
|
||||
const alloc = self.terminal.gpa();
|
||||
var cmd = self.apc_handler.end() orelse return;
|
||||
defer cmd.deinit(alloc);
|
||||
|
||||
switch (cmd) {
|
||||
var result = self.apc_handler.end() orelse return;
|
||||
defer result.deinit(alloc);
|
||||
switch (result) {
|
||||
.unknown => |*unknown| {
|
||||
if (terminated) self.unknownSequence(.{ .apc = .{
|
||||
.content = unknown.content,
|
||||
.truncated = unknown.truncated,
|
||||
} });
|
||||
},
|
||||
.kitty => |*kitty_cmd| if (comptime build_options.kitty_graphics) {
|
||||
if (self.terminal.kittyGraphics(
|
||||
io,
|
||||
@@ -1016,6 +1044,51 @@ test "resize clears synchronized output on unchanged cell dimensions" {
|
||||
try testing.expectEqual(@as(u32, 432), t.height_px);
|
||||
}
|
||||
|
||||
test "unknown APC effect callback" {
|
||||
var t: Terminal = try .init(testing.io, testing.allocator, .{ .cols = 80, .rows = 24 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
const S = struct {
|
||||
var count: usize = 0;
|
||||
var content: [16]u8 = undefined;
|
||||
var content_len: usize = undefined;
|
||||
var truncated: bool = undefined;
|
||||
|
||||
fn unknownSequence(_: *Handler, value: Handler.UnknownSequence) void {
|
||||
switch (value) {
|
||||
.apc => |apc_value| {
|
||||
content_len = apc_value.content.len;
|
||||
@memcpy(content[0..apc_value.content.len], apc_value.content);
|
||||
truncated = apc_value.truncated;
|
||||
},
|
||||
}
|
||||
count += 1;
|
||||
}
|
||||
};
|
||||
S.count = 0;
|
||||
|
||||
var handler: Handler = .init(&t);
|
||||
handler.unknown_sequence = &S.unknownSequence;
|
||||
handler.apc_handler.unknown_max_bytes = 8;
|
||||
var s: Stream = .init(.{
|
||||
.allocator = testing.allocator,
|
||||
.handler = handler,
|
||||
});
|
||||
defer s.deinit();
|
||||
|
||||
// Unknown OSC commands retain their legacy behavior and are ignored.
|
||||
s.nextSlice("\x1B]999;abcdef\x07");
|
||||
s.nextSlice("\x1B_abcd;payload\x1B\\");
|
||||
|
||||
try testing.expectEqual(@as(usize, 1), S.count);
|
||||
try testing.expectEqualStrings("abcd;pay", S.content[0..S.content_len]);
|
||||
try testing.expect(S.truncated);
|
||||
|
||||
// Aborted unknown APCs are suppressed.
|
||||
s.nextSlice("\x1B_Xpayload\x18");
|
||||
try testing.expectEqual(@as(usize, 1), S.count);
|
||||
}
|
||||
|
||||
test "resize reports mode 2048 geometry" {
|
||||
var t: Terminal = try .init(testing.io, testing.allocator, .{ .cols = 80, .rows = 24 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
@@ -476,11 +476,12 @@ pub const StreamHandler = struct {
|
||||
}
|
||||
|
||||
pub fn apcEnd(self: *StreamHandler) !void {
|
||||
var cmd = self.apc.end() orelse return;
|
||||
defer cmd.deinit(self.alloc);
|
||||
var result = self.apc.end() orelse return;
|
||||
defer result.deinit(self.alloc);
|
||||
|
||||
// log.warn("APC command: {}", .{cmd});
|
||||
switch (cmd) {
|
||||
// log.warn("APC command: {}", .{result});
|
||||
switch (result) {
|
||||
.unknown => return,
|
||||
.kitty => |*kitty_cmd| {
|
||||
if (self.terminal.kittyGraphics(global.io(), self.alloc, kitty_cmd)) |resp| {
|
||||
var buf: [1024]u8 = undefined;
|
||||
|
||||
Reference in New Issue
Block a user