mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 00:36:07 +03:00
terminal: generalized APC handler
This commit is contained in:
137
src/terminal/apc.zig
Normal file
137
src/terminal/apc.zig
Normal 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);
|
||||
}
|
@ -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");
|
||||
|
@ -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 {
|
||||
|
Reference in New Issue
Block a user