diff --git a/src/terminal/Parser.zig b/src/terminal/Parser.zig index 500306201..154dfee22 100644 --- a/src/terminal/Parser.zig +++ b/src/terminal/Parser.zig @@ -218,6 +218,10 @@ pub fn init() Parser { return .{}; } +pub fn deinit(self: *Parser) void { + self.osc_parser.deinit(); +} + /// Next consumes the next character c and returns the actions to execute. /// Up to 3 actions may need to be executed -- in order -- representing /// the state exit, transition, and entry actions. diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index c8f42b66a..823f9ff67 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -6,6 +6,8 @@ const osc = @This(); const std = @import("std"); const mem = std.mem; +const assert = std.debug.assert; +const Allocator = mem.Allocator; const log = std.log.scoped(.osc); @@ -145,6 +147,11 @@ pub const Terminator = enum { }; pub const Parser = struct { + /// Optional allocator used to accept data longer than MAX_BUF. + /// This only applies to some commands (e.g. OSC 52) that can + /// reasonably exceed MAX_BUF. + alloc: ?Allocator = null, + /// Current state of the parser. state: State = .empty, @@ -156,6 +163,7 @@ pub const Parser = struct { buf: [MAX_BUF]u8 = undefined, buf_start: usize = 0, buf_idx: usize = 0, + buf_dynamic: ?*std.ArrayListUnmanaged(u8) = null, /// True when a command is complete/valid to return. complete: bool = false, @@ -219,14 +227,30 @@ pub const Parser = struct { // Expect a string parameter. param_str must be set as well as // buf_start. string, + + // A string that can grow beyond MAX_BUF. This uses the allocator. + // If the parser has no allocator then it is treated as if the + // buffer is full. + allocable_string, }; + /// This must be called to clean up any allocated memory. + pub fn deinit(self: *Parser) void { + self.reset(); + } + /// Reset the parser start. pub fn reset(self: *Parser) void { self.state = .empty; self.buf_start = 0; self.buf_idx = 0; self.complete = false; + if (self.buf_dynamic) |ptr| { + const alloc = self.alloc.?; + ptr.deinit(alloc); + alloc.destroy(ptr); + self.buf_dynamic = null; + } } /// Consume the next character c and advance the parser state. @@ -351,9 +375,9 @@ pub const Parser = struct { .clipboard_kind => switch (c) { ';' => { self.command.clipboard_contents.kind = 'c'; - self.state = .string; self.temp_state = .{ .str = &self.command.clipboard_contents.data }; self.buf_start = self.buf_idx; + self.prepAllocableString(); }, else => { self.command.clipboard_contents.kind = c; @@ -363,9 +387,9 @@ pub const Parser = struct { .clipboard_kind_end => switch (c) { ';' => { - self.state = .string; self.temp_state = .{ .str = &self.command.clipboard_contents.data }; self.buf_start = self.buf_idx; + self.prepAllocableString(); }, else => self.state = .invalid, }, @@ -467,10 +491,46 @@ pub const Parser = struct { else => self.state = .invalid, }, + .allocable_string => { + const alloc = self.alloc.?; + const list = self.buf_dynamic.?; + list.append(alloc, c) catch { + self.state = .invalid; + return; + }; + + // Never consume buffer space for allocable strings + self.buf_idx -= 1; + + // We can complete at any time + self.complete = true; + }, + .string => self.complete = true, } } + fn prepAllocableString(self: *Parser) void { + assert(self.buf_dynamic == null); + + // We need an allocator. If we don't have an allocator, we + // pretend we're just a fixed buffer string and hope we fit! + const alloc = self.alloc orelse { + self.state = .string; + return; + }; + + // Allocate our dynamic buffer + const list = alloc.create(std.ArrayListUnmanaged(u8)) catch { + self.state = .string; + return; + }; + list.* = .{}; + + self.buf_dynamic = list; + self.state = .allocable_string; + } + fn endSemanticOptionValue(self: *Parser) void { const value = self.buf[self.buf_start..self.buf_idx]; @@ -531,6 +591,11 @@ pub const Parser = struct { self.temp_state.str.* = self.buf[self.buf_start..self.buf_idx]; } + fn endAllocableString(self: *Parser) void { + const list = self.buf_dynamic.?; + self.temp_state.str.* = list.items; + } + /// End the sequence and return the command, if any. If the return value /// is null, then no valid command was found. The optional terminator_ch /// is the final character in the OSC sequence. This is used to determine @@ -546,6 +611,7 @@ pub const Parser = struct { .semantic_exit_code => self.endSemanticExitCode(), .semantic_option_value => self.endSemanticOptionValue(), .string => self.endString(), + .allocable_string => self.endAllocableString(), else => {}, } @@ -762,6 +828,22 @@ test "OSC: get/set clipboard (optional parameter)" { try testing.expect(std.mem.eql(u8, "?", cmd.clipboard_contents.data)); } +test "OSC: get/set clipboard with allocator" { + const testing = std.testing; + const alloc = testing.allocator; + + var p: Parser = .{ .alloc = alloc }; + defer p.deinit(); + + const input = "52;s;?"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?; + try testing.expect(cmd == .clipboard_contents); + try testing.expect(cmd.clipboard_contents.kind == 's'); + try testing.expect(std.mem.eql(u8, "?", cmd.clipboard_contents.data)); +} + test "OSC: report pwd" { const testing = std.testing; diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index d3bc372a1..d3ab59e37 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -38,6 +38,10 @@ pub fn Stream(comptime Handler: type) type { handler: Handler, parser: Parser = .{}, + pub fn deinit(self: *Self) void { + self.parser.deinit(); + } + /// Process a string of characters. pub fn nextSlice(self: *Self, c: []const u8) !void { const tracy = trace(@src()); diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index c25b70031..69bbb65eb 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -229,6 +229,14 @@ pub fn threadEnter(self: *Exec, thread: *termio.Thread) !ThreadData { .default_background_color = self.default_background_color, .osc_color_report_format = self.osc_color_report_format, }, + + .parser = .{ + .osc_parser = .{ + // Populate the OSC parser allocator (optional) because + // we want to support large OSC payloads such as OSC 52. + .alloc = self.alloc, + }, + }, }, }; errdefer ev_data_ptr.deinit(self.alloc); @@ -565,6 +573,7 @@ const EventData = struct { // Clear any StreamHandler state self.terminal_stream.handler.deinit(); + self.terminal_stream.deinit(); } /// This queues a render operation with the renderer thread. The render