diff --git a/src/Surface.zig b/src/Surface.zig index d730f8e0e..252968ccc 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -2298,7 +2298,11 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool // 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}), + else => unreachable, + }; _ = self.io_thread.mailbox.push(try termio.Message.writeReq( self.alloc, full_data, @@ -2315,6 +2319,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/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 6a66e1d6f..460e39dfc 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -134,6 +134,12 @@ pub const Action = union(enum) { /// Send an ESC sequence. esc: []const u8, + // 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 /// enabled ("application") or disabled ("normal"). cursor_key: CursorKey,