From f7e622e8af105984f62a40016b9c8beeb122e244 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 16 Sep 2025 13:30:19 -0700 Subject: [PATCH] config: fix binding parsing to allow values containing `=` Fixes #8667 The binding `a=text:=` didn't parse properly. This is a band-aid solution. It works and we have test coverage for it thankfully. Longer term we should move the parser to a fully state-machine based parser that parses the trigger first then the action, to avoid these kind of things. --- src/input/Binding.zig | 67 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 62 insertions(+), 5 deletions(-) diff --git a/src/input/Binding.zig b/src/input/Binding.zig index b0e4e918d..54e7754f2 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -64,11 +64,35 @@ pub const Parser = struct { const flags, const start_idx = try parseFlags(raw_input); const input = raw_input[start_idx..]; - // Find the last = which splits are mapping into the trigger - // and action, respectively. - // We use the last = because the keybind itself could contain - // raw equal signs (for the = codepoint) - const eql_idx = std.mem.lastIndexOf(u8, input, "=") orelse return Error.InvalidFormat; + // Find the equal sign. This is more complicated than it seems on + // the surface because we need to ignore equal signs that are + // part of the trigger. + const eql_idx: usize = eql: { + // TODO: We should change this parser into a real state machine + // based parser that parses the trigger fully, then yields the + // action after. The loop below is a total mess. + var offset: usize = 0; + while (std.mem.indexOfScalar( + u8, + input[offset..], + '=', + )) |offset_idx| { + // Find: '=+ctrl' or '==action' + const idx = offset + offset_idx; + if (idx < input.len - 1 and + (input[idx + 1] == '+' or + input[idx + 1] == '=')) + { + offset += offset_idx + 1; + continue; + } + + // Looks like the real equal sign. + break :eql idx; + } + + return Error.InvalidFormat; + }; // Sequence iterator goes up to the equal, action is after. We can // parse the action now. @@ -2298,6 +2322,39 @@ test "parse: equals sign" { try testing.expectError(Error.InvalidFormat, parseSingle("=ignore")); } +test "parse: text action equals sign" { + const testing = std.testing; + { + const binding = try parseSingle("==text:="); + try testing.expectEqual(Trigger{ .key = .{ .unicode = '=' } }, binding.trigger); + try testing.expectEqualStrings("=", binding.action.text); + } + + { + const binding = try parseSingle("==text:=hello"); + try testing.expectEqual(Trigger{ .key = .{ .unicode = '=' } }, binding.trigger); + try testing.expectEqualStrings("=hello", binding.action.text); + } + + { + const binding = try parseSingle("ctrl+==text:=hello"); + try testing.expectEqual(Trigger{ + .key = .{ .unicode = '=' }, + .mods = .{ .ctrl = true }, + }, binding.trigger); + try testing.expectEqualStrings("=hello", binding.action.text); + } + + { + const binding = try parseSingle("=+ctrl=text:=hello"); + try testing.expectEqual(Trigger{ + .key = .{ .unicode = '=' }, + .mods = .{ .ctrl = true }, + }, binding.trigger); + try testing.expectEqualStrings("=hello", binding.action.text); + } +} + // For Ghostty 1.2+ we changed our key names to match the W3C and removed // `physical:`. This tests the backwards compatibility with the old format. // Note that our backwards compatibility isn't 100% perfect since triggers