mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +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 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 point = @import("point.zig");
|
pub const point = @import("point.zig");
|
||||||
pub const color = @import("color.zig");
|
pub const color = @import("color.zig");
|
||||||
pub const kitty = @import("kitty.zig");
|
pub const kitty = @import("kitty.zig");
|
||||||
|
@ -1048,15 +1048,11 @@ const StreamHandler = struct {
|
|||||||
grid_size: *renderer.GridSize,
|
grid_size: *renderer.GridSize,
|
||||||
terminal: *terminal.Terminal,
|
terminal: *terminal.Terminal,
|
||||||
|
|
||||||
/// The APC command data. This is always heap-allocated and freed on
|
/// The APC command handler maintains the APC state. APC is like
|
||||||
/// apcEnd because APC commands are so very rare.
|
/// CSI or OSC, but it is a private escape sequence that is used
|
||||||
apc_data: std.ArrayListUnmanaged(u8) = .{},
|
/// to send commands to the terminal emulator. This is used by
|
||||||
apc_state: enum {
|
/// the kitty graphics protocol.
|
||||||
inactive,
|
apc: terminal.apc.Handler = .{},
|
||||||
ignore,
|
|
||||||
verify,
|
|
||||||
collect,
|
|
||||||
} = .inactive,
|
|
||||||
|
|
||||||
/// 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
|
||||||
@ -1064,7 +1060,7 @@ const StreamHandler = struct {
|
|||||||
writer_messaged: bool = false,
|
writer_messaged: bool = false,
|
||||||
|
|
||||||
pub fn deinit(self: *StreamHandler) void {
|
pub fn deinit(self: *StreamHandler) void {
|
||||||
self.apc_data.deinit(self.alloc);
|
self.apc.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fn queueRender(self: *StreamHandler) !void {
|
inline fn queueRender(self: *StreamHandler) !void {
|
||||||
@ -1077,49 +1073,17 @@ const StreamHandler = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn apcStart(self: *StreamHandler) !void {
|
pub fn apcStart(self: *StreamHandler) !void {
|
||||||
assert(self.apc_data.items.len == 0);
|
self.apc.start();
|
||||||
self.apc_state = .verify;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn apcPut(self: *StreamHandler, byte: u8) !void {
|
pub fn apcPut(self: *StreamHandler, byte: u8) !void {
|
||||||
switch (self.apc_state) {
|
self.apc.feed(self.alloc, byte);
|
||||||
.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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn apcEnd(self: *StreamHandler) !void {
|
pub fn apcEnd(self: *StreamHandler) !void {
|
||||||
self.apc_state = .inactive;
|
var cmd = self.apc.end() orelse return;
|
||||||
self.apc_data.clearAndFree(self.alloc);
|
defer cmd.deinit(self.alloc);
|
||||||
|
log.warn("APC command: {}", .{cmd});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn print(self: *StreamHandler, ch: u21) !void {
|
pub fn print(self: *StreamHandler, ch: u21) !void {
|
||||||
|
Reference in New Issue
Block a user