From 223accb4c8d09806fe4a155ea8d0a6b463c33cd0 Mon Sep 17 00:00:00 2001 From: Guillaume Wenzek Date: Fri, 24 Nov 2023 17:21:57 +0100 Subject: [PATCH 1/5] add "text" action --- src/Surface.zig | 42 ++++++++++++++++++++++++++++++++++++++++-- src/input/Binding.zig | 3 +++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 308e6e878..97129b0e5 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -2254,6 +2254,39 @@ 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. /// @@ -2271,12 +2304,17 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool .reload_config => try self.app.reloadConfig(self.rt_app), - .csi, .esc => |data| { + .csi, .esc, .text => |data| { // We need to send the CSI/ESC sequence as a single write request. // If you split it across two then the shell can interpret it // as two literals. var buf: [128]u8 = undefined; - const full_data = try std.fmt.bufPrint(&buf, "\x1b{s}{s}", .{ if (action == .csi) "[" else "", data }); + 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), + else => unreachable, + }; _ = self.io_thread.mailbox.push(try termio.Message.writeReq( self.alloc, full_data, diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 6a66e1d6f..37e93ae3c 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -134,6 +134,9 @@ pub const Action = union(enum) { /// Send an ESC sequence. esc: []const u8, + // Send the given text. Uses Zig string literal syntax. + text: []const u8, + /// Send data to the pty depending on whether cursor key mode is /// enabled ("application") or disabled ("normal"). cursor_key: CursorKey, From 9c3e2b4ddd546e5f0c9d60c25a22bc5ab0be1283 Mon Sep 17 00:00:00 2001 From: Guillaume Wenzek Date: Fri, 24 Nov 2023 17:48:52 +0100 Subject: [PATCH 2/5] parse string literal at load time --- src/Surface.zig | 35 +---------------------------- src/input/Binding.zig | 52 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 52 insertions(+), 35 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 97129b0e5..22853908a 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,7 @@ 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 => data, else => unreachable, }; _ = self.io_thread.mailbox.push(try termio.Message.writeReq( diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 37e93ae3c..4b4ef03dd 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -321,6 +321,47 @@ pub const Action = union(enum) { }; } + // Use Zig syntax to parse a string literal. + // Since the escaping syntax is verbose, we can do the parsing inplace. + pub fn parseStringLiteralInPlace(bytes: []const u8) []const u8 { + var out: [128]u8 = undefined; + var offset: usize = 0; + var index: usize = 0; + while (true) { + assert(offset <= index); + 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; + }, + } + } + // No escaping => no copy needed. + if (offset == index and index == bytes.len) return bytes; + const mut_bytes: []u8 = @as([*]u8, @ptrFromInt(@intFromPtr(bytes.ptr)))[0..offset]; + @memcpy(mut_bytes, out[0..offset]); + return mut_bytes; + } + /// Parse an action in the format of "key=value" where key is the /// action name and value is the action parameter. The parameter /// is optional depending on the action. @@ -346,7 +387,10 @@ pub const Action = union(enum) { []const u8 => { const idx = colonIdx orelse return Error.InvalidFormat; - const param = input[idx + 1 ..]; + const param = if (std.mem.eql(u8, field.name, "text")) + parseStringLiteralInPlace(input[idx + 1 ..]) + else + input[idx + 1 ..]; return @unionInit(Action, field.name, param); }, @@ -852,6 +896,12 @@ test "parse: action with string" { try testing.expect(binding.action == .esc); try testing.expectEqualStrings("A", binding.action.esc); } + // parameter + { + const binding = try parse("a=text:\\x03\\u{26a1}"); + try testing.expect(binding.action == .text); + try testing.expectEqualStrings(binding.action.text, &[_]u8{ 3, 0xe2, 0x9a, 0xa1 }); + } } test "parse: action with enum" { From 2e02083eef1e11aa015637ee60beccb79cef9f88 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 24 Nov 2023 10:06:34 -0800 Subject: [PATCH 3/5] Revert "parse string literal at load time" This reverts commit 9c3e2b4ddd546e5f0c9d60c25a22bc5ab0be1283. --- src/Surface.zig | 35 ++++++++++++++++++++++++++++- src/input/Binding.zig | 52 +------------------------------------------ 2 files changed, 35 insertions(+), 52 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 22853908a..97129b0e5 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -2254,6 +2254,39 @@ 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. /// @@ -2279,7 +2312,7 @@ 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 => data, + .text => parseStringLiteral(&buf, data), else => unreachable, }; _ = self.io_thread.mailbox.push(try termio.Message.writeReq( diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 4b4ef03dd..37e93ae3c 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -321,47 +321,6 @@ pub const Action = union(enum) { }; } - // Use Zig syntax to parse a string literal. - // Since the escaping syntax is verbose, we can do the parsing inplace. - pub fn parseStringLiteralInPlace(bytes: []const u8) []const u8 { - var out: [128]u8 = undefined; - var offset: usize = 0; - var index: usize = 0; - while (true) { - assert(offset <= index); - 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; - }, - } - } - // No escaping => no copy needed. - if (offset == index and index == bytes.len) return bytes; - const mut_bytes: []u8 = @as([*]u8, @ptrFromInt(@intFromPtr(bytes.ptr)))[0..offset]; - @memcpy(mut_bytes, out[0..offset]); - return mut_bytes; - } - /// Parse an action in the format of "key=value" where key is the /// action name and value is the action parameter. The parameter /// is optional depending on the action. @@ -387,10 +346,7 @@ pub const Action = union(enum) { []const u8 => { const idx = colonIdx orelse return Error.InvalidFormat; - const param = if (std.mem.eql(u8, field.name, "text")) - parseStringLiteralInPlace(input[idx + 1 ..]) - else - input[idx + 1 ..]; + const param = input[idx + 1 ..]; return @unionInit(Action, field.name, param); }, @@ -896,12 +852,6 @@ test "parse: action with string" { try testing.expect(binding.action == .esc); try testing.expectEqualStrings("A", binding.action.esc); } - // parameter - { - const binding = try parse("a=text:\\x03\\u{26a1}"); - try testing.expect(binding.action == .text); - try testing.expectEqualStrings(binding.action.text, &[_]u8{ 3, 0xe2, 0x9a, 0xa1 }); - } } test "parse: action with enum" { From 0e2970bdebcec18ee9b7603823425dcebbe83da3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 24 Nov 2023 10:26:55 -0800 Subject: [PATCH 4/5] config: add string parse, tests --- src/Surface.zig | 38 +++--------------------- src/config.zig | 1 + src/config/string.zig | 67 +++++++++++++++++++++++++++++++++++++++++++ src/input/Binding.zig | 5 +++- 4 files changed, 76 insertions(+), 35 deletions(-) create mode 100644 src/config/string.zig 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 From 6fc0d2d4a84c4c0264307963cfe2f262e76dfc95 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 24 Nov 2023 10:36:43 -0800 Subject: [PATCH 5/5] input: allocate for text bindings --- src/Surface.zig | 34 +++++++++++++++++++++++++++++----- src/input/Binding.zig | 8 ++++---- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index c917d07ee..2ec42f026 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -2271,7 +2271,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool .reload_config => try self.app.reloadConfig(self.rt_app), - .csi, .esc, .text => |data| { + .csi, .esc => |data| { // We need to send the CSI/ESC sequence as a single write request. // If you split it across two then the shell can interpret it // as two literals. @@ -2279,10 +2279,6 @@ 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 => 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( @@ -2301,6 +2297,34 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool } }, + .text => |data| { + // For text we always allocate just because its easier to + // handle all cases that way. + var buf = try self.alloc.alloc(u8, data.len); + defer self.alloc.free(buf); + const text = configpkg.string.parse(buf, data) catch |err| { + log.warn( + "error parsing text binding text={s} err={}", + .{ data, err }, + ); + return true; + }; + _ = self.io_thread.mailbox.push(try termio.Message.writeReq( + self.alloc, + text, + ), .{ .forever = {} }); + try self.io_thread.wakeup.notify(); + + // Text triggers a scroll. + { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + self.scrollToBottom() catch |err| { + log.warn("error scrolling to bottom err={}", .{err}); + }; + } + }, + .cursor_key => |ck| { // We send a different sequence depending on if we're // in cursor keys mode. We're in "normal" mode if cursor diff --git a/src/input/Binding.zig b/src/input/Binding.zig index dba892994..460e39dfc 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -134,10 +134,10 @@ pub const Action = union(enum) { /// Send an ESC sequence. esc: []const u8, - // 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. + // Send the given text. Uses Zig string literal syntax. 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