ensure `ctrl++` parses, clarify case folding docs

pull/7320/head
Mitchell Hashimoto 2025-05-12 09:07:53 -07:00
parent c4f1c78fcf
commit 8f40d1331e
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
2 changed files with 54 additions and 4 deletions

View File

@ -935,6 +935,22 @@ class: ?[:0]const u8 = null,
/// QWERTY keyboard, but will match the `q` key on a AZERTY keyboard
/// (assuming US physical layout).
///
/// For Unicode codepoints, matching is done by comparing the set of
/// modifiers with the unmodified codepoint. The unmodified codepoint is
/// sometimes called an "unshifted character" in other software, but all
/// modifiers are considered, not only shift. For example, `ctrl+a` will match
/// `a` but not `ctrl+shift+a` (which is `A` on a US keyboard).
///
/// Further, codepoint matching is case-insensitive and the unmodified
/// codepoint is always case folded for comparison. As a result,
/// `ctrl+A` configured will match when `ctrl+a` is pressed. Note that
/// this means some key combinations are impossible depending on keyboard
/// layout. For example, `ctrl+_` is impossible on a US keyboard because
/// `_` is `shift+-` and `ctrl+shift+-` is not equal to `ctrl+_` (because
/// the modifiers don't match!). More details on impossible key combinations
/// can be found at this excellent source written by Qt developers:
/// https://doc.qt.io/qt-6/qkeysequence.html#keyboard-layout-issues
///
/// Physical key codes can be specified by using any of the key codes
/// as specified by the [W3C specification](https://www.w3.org/TR/uievents-code/).
/// For example, `KeyA` will match the physical `a` key on a US standard

View File

@ -1109,10 +1109,11 @@ pub const Trigger = struct {
pub fn parse(input: []const u8) !Trigger {
if (input.len == 0) return Error.InvalidFormat;
var result: Trigger = .{};
var iter = std.mem.tokenizeScalar(u8, input, '+');
loop: while (iter.next()) |part| {
// All parts must be non-empty
if (part.len == 0) return Error.InvalidFormat;
var rem: []const u8 = input;
loop: while (rem.len > 0) {
const idx = std.mem.indexOfScalar(u8, rem, '+') orelse rem.len;
const part = rem[0..idx];
rem = if (idx >= rem.len) "" else rem[idx + 1 ..];
// Check if its a modifier
const modsInfo = @typeInfo(key.Mods).@"struct";
@ -1148,6 +1149,13 @@ pub const Trigger = struct {
// single keys.
if (!result.isKeyUnset()) return Error.InvalidFormat;
// If the part is empty it means that it is actually
// a literal `+`, which we treat as a Unicode character.
if (part.len == 0) {
result.key = .{ .unicode = '+' };
continue :loop;
}
// Check if its a key
const keysInfo = @typeInfo(key.Key).@"enum";
inline for (keysInfo.fields) |field| {
@ -2000,6 +2008,32 @@ test "parse: w3c key names" {
try testing.expectError(Error.InvalidFormat, parseSingle("Keya=ignore"));
}
test "parse: plus sign" {
const testing = std.testing;
try testing.expectEqual(
Binding{
.trigger = .{ .key = .{ .unicode = '+' } },
.action = .{ .ignore = {} },
},
try parseSingle("+=ignore"),
);
// Modifier
try testing.expectEqual(
Binding{
.trigger = .{
.key = .{ .unicode = '+' },
.mods = .{ .ctrl = true },
},
.action = .{ .ignore = {} },
},
try parseSingle("ctrl++=ignore"),
);
try testing.expectError(Error.InvalidFormat, parseSingle("++=ignore"));
}
// 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