diff --git a/src/Surface.zig b/src/Surface.zig index 97129b0e5..c917d07ee 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -2254,39 +2254,6 @@ fn showMouse(self: *Surface) void { self.rt_surface.setMouseVisibility(true); } -pub fn parseStringLiteral(out: []u8, bytes: []const u8) []u8 { - var offset: usize = 0; - var index: usize = 0; - while (true) { - if (index >= bytes.len or offset >= out.len) break; - const b = bytes[index]; - switch (b) { - '\\' => { - const escape_char_index = index + 1; - const result = std.zig.string_literal.parseEscapeSequence(bytes, &index); - switch (result) { - .success => |codepoint| { - if (bytes[escape_char_index] == 'u') { - const len = std.unicode.utf8Encode(codepoint, out[offset..]) catch break; - offset += len; - } else { - out[offset] = @as(u8, @intCast(codepoint)); - offset += 1; - } - }, - .failure => break, - } - }, - else => { - out[offset] = b; - offset += 1; - index += 1; - }, - } - } - return out[0..offset]; -} - /// Perform a binding action. A binding is a keybinding. This function /// must be called from the GUI thread. /// @@ -2312,7 +2279,10 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool const full_data = switch (action) { .csi => try std.fmt.bufPrint(&buf, "\x1b[{s}", .{data}), .esc => try std.fmt.bufPrint(&buf, "\x1b{s}", .{data}), - .text => parseStringLiteral(&buf, data), + .text => configpkg.string.parse(&buf, data) catch |err| { + log.warn("error parsing text binding text={s} err={}", .{ data, err }); + return true; + }, else => unreachable, }; _ = self.io_thread.mailbox.push(try termio.Message.writeReq( diff --git a/src/config.zig b/src/config.zig index 6834291e7..e639f9b84 100644 --- a/src/config.zig +++ b/src/config.zig @@ -2,6 +2,7 @@ const builtin = @import("builtin"); pub usingnamespace @import("config/key.zig"); pub const Config = @import("config/Config.zig"); +pub const string = @import("config/string.zig"); // Field types pub const CopyOnSelect = Config.CopyOnSelect; diff --git a/src/config/string.zig b/src/config/string.zig new file mode 100644 index 000000000..5e0d40e55 --- /dev/null +++ b/src/config/string.zig @@ -0,0 +1,67 @@ +const std = @import("std"); + +/// Parse a string literal into a byte array. The string can contain +/// any valid Zig string literal escape sequences. +/// +/// The output buffer never needs sto be larger than the input buffer. +/// The buffers may alias. +pub fn parse(out: []u8, bytes: []const u8) ![]u8 { + var dst_i: usize = 0; + var src_i: usize = 0; + while (src_i < bytes.len) { + if (dst_i >= out.len) return error.OutOfMemory; + + // If this byte is not beginning an escape sequence we copy. + const b = bytes[src_i]; + if (b != '\\') { + out[dst_i] = b; + dst_i += 1; + src_i += 1; + continue; + } + + // Parse the escape sequence + switch (std.zig.string_literal.parseEscapeSequence( + bytes, + &src_i, + )) { + .failure => return error.InvalidString, + .success => |cp| dst_i += try std.unicode.utf8Encode( + cp, + out[dst_i..], + ), + } + } + + return out[0..dst_i]; +} + +test "parse: empty" { + const testing = std.testing; + + var buf: [128]u8 = undefined; + const result = try parse(&buf, ""); + try testing.expectEqualStrings("", result); +} + +test "parse: no escapes" { + const testing = std.testing; + + var buf: [128]u8 = undefined; + const result = try parse(&buf, "hello world"); + try testing.expectEqualStrings("hello world", result); +} + +test "parse: escapes" { + const testing = std.testing; + + var buf: [128]u8 = undefined; + { + const result = try parse(&buf, "hello\\nworld"); + try testing.expectEqualStrings("hello\nworld", result); + } + { + const result = try parse(&buf, "hello\\u{1F601}world"); + try testing.expectEqualStrings("hello\u{1F601}world", result); + } +} diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 37e93ae3c..dba892994 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -134,7 +134,10 @@ pub const Action = union(enum) { /// Send an ESC sequence. esc: []const u8, - // Send the given text. Uses Zig string literal syntax. + // Send the given text. Uses Zig string literal syntax. The maximum + // length of the string is 128 bytes. This is currently not validated. + // If the text is invalid (i.e. contains an invalid escape sequence), + // the error will currently only show up in logs. text: []const u8, /// Send data to the pty depending on whether cursor key mode is