diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 590d7ad74..4e3774e4d 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -66,9 +66,15 @@ pub fn Stream(comptime Handler: type) type { .dcs_hook => |dcs| log.warn("unhandled DCS hook: {}", .{dcs}), .dcs_put => |code| log.warn("unhandled DCS put: {x}", .{code}), .dcs_unhook => log.warn("unhandled DCS unhook", .{}), - .apc_start => log.warn("unhandled APC start", .{}), - .apc_put => |code| log.warn("unhandled APC put: {x}", .{code}), - .apc_end => log.warn("unhandled APC end", .{}), + .apc_start => if (@hasDecl(T, "apcStart")) { + try self.handler.apcStart(); + } else log.warn("unimplemented APC start", .{}), + .apc_put => |code| if (@hasDecl(T, "apcPut")) { + try self.handler.apcPut(code); + } else log.warn("unimplemented APC put: {x}", .{code}), + .apc_end => if (@hasDecl(T, "apcEnd")) { + try self.handler.apcEnd(); + } else log.warn("unimplemented APC end", .{}), } } } diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 7e34bac7a..6e24217f8 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -47,10 +47,6 @@ subprocess: Subprocess, /// just stores internal state about a grid. terminal: terminal.Terminal, -/// The stream parser. This parses the stream of escape codes and so on -/// from the child process and calls callbacks in the stream handler. -terminal_stream: terminal.Stream(StreamHandler), - /// The shared render state renderer_state: *renderer.State, @@ -114,7 +110,6 @@ pub fn init(alloc: Allocator, opts: termio.Options) !Exec { return Exec{ .alloc = alloc, .terminal = term, - .terminal_stream = undefined, .subprocess = subprocess, .renderer_state = opts.renderer_state, .renderer_wakeup = opts.renderer_wakeup, @@ -445,6 +440,9 @@ const EventData = struct { // Stop our process watcher self.process.deinit(); + + // Clear any StreamHandler state + self.terminal_stream.handler.deinit(); } /// This queues a render operation with the renderer thread. The render @@ -1050,11 +1048,25 @@ 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, + /// 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 /// to wake up the writer. writer_messaged: bool = false, + pub fn deinit(self: *StreamHandler) void { + self.apc_data.deinit(self.alloc); + } + inline fn queueRender(self: *StreamHandler) !void { try self.ev.queueRender(); } @@ -1064,6 +1076,44 @@ const StreamHandler = struct { self.writer_messaged = true; } + pub fn apcStart(self: *StreamHandler) !void { + assert(self.apc_data.items.len == 0); + self.apc_state = .verify; + } + + 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); + } + + pub fn apcEnd(self: *StreamHandler) !void { + self.apc_state = .inactive; + self.apc_data.clearAndFree(self.alloc); + } + pub fn print(self: *StreamHandler, ch: u21) !void { try self.terminal.print(ch); }