terminal: generalized APC handler

This commit is contained in:
Mitchell Hashimoto
2023-08-20 11:16:12 -07:00
parent b7f819dc0b
commit 824d586afc
3 changed files with 149 additions and 47 deletions

137
src/terminal/apc.zig Normal file
View File

@ -0,0 +1,137 @@
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const kitty_gfx = @import("kitty/graphics.zig");
const log = std.log.scoped(.terminal_apc);
/// APC command handler. This should be hooked into a terminal.Stream handler.
/// The start/feed/end functions are meant to be called from the terminal.Stream
/// apcStart, apcPut, and apcEnd functions, respectively.
pub const Handler = struct {
state: State = .{ .inactive = {} },
pub fn deinit(self: *Handler) void {
self.state.deinit();
}
pub fn start(self: *Handler) void {
self.state.deinit();
self.state = .{ .identify = {} };
}
pub fn feed(self: *Handler, alloc: Allocator, byte: u8) void {
switch (self.state) {
.inactive => unreachable,
// We're ignoring this APC command, likely because we don't
// recognize it so there is no need to store the data in memory.
.ignore => return,
// We identify the APC command by the first byte.
.identify => {
switch (byte) {
// Kitty graphics protocol
'G' => self.state = .{ .kitty = kitty_gfx.CommandParser.init(alloc) },
// Unknown
else => self.state = .{ .ignore = {} },
}
},
.kitty => |*p| p.feed(byte) catch |err| {
log.warn("kitty graphics protocol error: {}", .{err});
self.state = .{ .ignore = {} };
},
}
}
pub fn end(self: *Handler) ?Command {
defer {
self.state.deinit();
self.state = .{ .inactive = {} };
}
return switch (self.state) {
.inactive => unreachable,
.ignore, .identify => null,
.kitty => |*p| kitty: {
const command = p.complete() catch |err| {
log.warn("kitty graphics protocol error: {}", .{err});
break :kitty null;
};
break :kitty .{ .kitty = command };
},
};
}
};
pub const State = union(enum) {
/// We're not in the middle of an APC command yet.
inactive: void,
/// We got an unrecognized APC sequence or the APC sequence we
/// recognized became invalid. We're just dropping bytes.
ignore: void,
/// We're waiting to identify the APC sequence. This is done by
/// inspecting the first byte of the sequence.
identify: void,
/// Kitty graphics protocol
kitty: kitty_gfx.CommandParser,
pub fn deinit(self: *State) void {
switch (self.*) {
.inactive, .ignore, .identify => {},
.kitty => |*v| v.deinit(),
}
}
};
/// Possible APC commands.
pub const Command = union(enum) {
kitty: kitty_gfx.Command,
pub fn deinit(self: *Command, alloc: Allocator) void {
switch (self.*) {
.kitty => |*v| v.deinit(alloc),
}
}
};
test "unknown APC command" {
const testing = std.testing;
const alloc = testing.allocator;
var h: Handler = .{};
h.start();
for ("Xabcdef1234") |c| h.feed(alloc, c);
try testing.expect(h.end() == null);
}
test "garbage Kitty command" {
const testing = std.testing;
const alloc = testing.allocator;
var h: Handler = .{};
h.start();
for ("Gabcdef1234") |c| h.feed(alloc, c);
try testing.expect(h.end() == null);
}
test "valid Kitty command" {
const testing = std.testing;
const alloc = testing.allocator;
var h: Handler = .{};
h.start();
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);
}

View File

@ -5,6 +5,7 @@ const stream = @import("stream.zig");
const ansi = @import("ansi.zig");
const csi = @import("csi.zig");
const sgr = @import("sgr.zig");
pub const apc = @import("apc.zig");
pub const point = @import("point.zig");
pub const color = @import("color.zig");
pub const kitty = @import("kitty.zig");

View File

@ -1048,15 +1048,11 @@ const StreamHandler = struct {
grid_size: *renderer.GridSize,
terminal: *terminal.Terminal,
/// The APC command data. This is always heap-allocated and freed on
/// apcEnd because APC commands are so very rare.
apc_data: std.ArrayListUnmanaged(u8) = .{},
apc_state: enum {
inactive,
ignore,
verify,
collect,
} = .inactive,
/// The APC command handler maintains the APC state. APC is like
/// CSI or OSC, but it is a private escape sequence that is used
/// to send commands to the terminal emulator. This is used by
/// the kitty graphics protocol.
apc: terminal.apc.Handler = .{},
/// 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
@ -1064,7 +1060,7 @@ const StreamHandler = struct {
writer_messaged: bool = false,
pub fn deinit(self: *StreamHandler) void {
self.apc_data.deinit(self.alloc);
self.apc.deinit();
}
inline fn queueRender(self: *StreamHandler) !void {
@ -1077,49 +1073,17 @@ const StreamHandler = struct {
}
pub fn apcStart(self: *StreamHandler) !void {
assert(self.apc_data.items.len == 0);
self.apc_state = .verify;
self.apc.start();
}
pub fn apcPut(self: *StreamHandler, byte: u8) !void {
switch (self.apc_state) {
.inactive => unreachable,
// We're ignoring this APC command, likely because we don't
// recognize it so there is no need to store the data in memory.
.ignore => return,
// Verify it is a command we expect
.verify => {
switch (byte) {
'G' => {},
else => {
log.warn("unrecognized APC command, first byte: {x}", .{byte});
self.apc_state = .ignore;
return;
},
}
self.apc_state = .collect;
},
.collect => {},
}
try self.apc_data.append(self.alloc, byte);
// Prevent DoS attack.
const limit = 100 * 1024 * 1024; // 100MB
if (self.apc_data.items.len > limit) {
log.warn("APC command too large, ignoring", .{});
self.apc_state = .ignore;
self.apc_data.clearAndFree(self.alloc);
}
self.apc.feed(self.alloc, byte);
}
pub fn apcEnd(self: *StreamHandler) !void {
self.apc_state = .inactive;
self.apc_data.clearAndFree(self.alloc);
var cmd = self.apc.end() orelse return;
defer cmd.deinit(self.alloc);
log.warn("APC command: {}", .{cmd});
}
pub fn print(self: *StreamHandler, ch: u21) !void {