diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index deb0a6987..ae3bd599a 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -905,10 +905,32 @@ extension Ghostty { // but this is super cheap and flagsChanged isn't that common. let mods = Ghostty.ghosttyMods(event.modifierFlags) - // If the key that pressed this is active, its a press, else release + // If the key that pressed this is active, its a press, else release. var action = GHOSTTY_ACTION_RELEASE - if (mods.rawValue & mod != 0) { action = GHOSTTY_ACTION_PRESS } - + if (mods.rawValue & mod != 0) { + // If the key is pressed, its slightly more complicated, because we + // want to check if the pressed modifier is the correct side. If the + // correct side is pressed then its a press event otherwise its a release + // event with the opposite modifier still held. + let sidePressed: Bool + switch (event.keyCode) { + case 0x3C: + sidePressed = event.modifierFlags.rawValue & UInt(NX_DEVICERSHIFTKEYMASK) != 0; + case 0x3E: + sidePressed = event.modifierFlags.rawValue & UInt(NX_DEVICERCTLKEYMASK) != 0; + case 0x3D: + sidePressed = event.modifierFlags.rawValue & UInt(NX_DEVICERALTKEYMASK) != 0; + case 0x36: + sidePressed = event.modifierFlags.rawValue & UInt(NX_DEVICERCMDKEYMASK) != 0; + default: + sidePressed = true + } + + if (sidePressed) { + action = GHOSTTY_ACTION_PRESS + } + } + keyAction(action, event: event) } diff --git a/src/input/KeyEncoder.zig b/src/input/KeyEncoder.zig index 8b7757b11..7bb4708ac 100644 --- a/src/input/KeyEncoder.zig +++ b/src/input/KeyEncoder.zig @@ -143,7 +143,11 @@ fn kitty( var seq: KittySequence = .{ .key = entry.code, .final = entry.final, - .mods = KittyMods.fromInput(all_mods), + .mods = KittyMods.fromInput( + self.event.action, + self.event.key, + all_mods, + ), }; if (self.kitty_flags.report_events) { @@ -596,12 +600,27 @@ const KittyMods = packed struct(u8) { num_lock: bool = false, /// Convert an input mods value into the CSI u mods value. - pub fn fromInput(mods: key.Mods) KittyMods { + pub fn fromInput( + action: key.Action, + k: key.Key, + mods: key.Mods, + ) KittyMods { + // Annoying boolean logic, but according to the Kitty spec: + // "When both left and right control keys are pressed and one is + // released, the release event must again have the modifier bit reset" + // In other words, we allow a modifier if it is set AND the action + // is NOT a release. Or if the action is a release, then the key being + // released must not be the associated modifier key. + const shift = mods.shift and (action != .release or (k != .left_shift and k != .right_shift)); + const alt = mods.alt and (action != .release or (k != .left_alt and k != .right_alt)); + const ctrl = mods.ctrl and (action != .release or (k != .left_control and k != .right_control)); + const super = mods.super and (action != .release or (k != .left_super and k != .right_super)); + return .{ - .shift = mods.shift, - .alt = mods.alt, - .ctrl = mods.ctrl, - .super = mods.super, + .shift = shift, + .alt = alt, + .ctrl = ctrl, + .super = super, .caps_lock = mods.caps_lock, .num_lock = mods.num_lock, }; @@ -982,6 +1001,27 @@ test "kitty: ctrl with all flags" { try testing.expectEqualStrings("[57442;5u", actual[1..]); } +test "kitty: ctrl release with ctrl mod set" { + var buf: [128]u8 = undefined; + var enc: KeyEncoder = .{ + .event = .{ + .action = .release, + .key = .left_control, + .mods = .{ .ctrl = true }, + .utf8 = "", + }, + .kitty_flags = .{ + .disambiguate = true, + .report_events = true, + .report_alternates = true, + .report_all = true, + .report_associated = true, + }, + }; + const actual = try enc.kitty(&buf); + try testing.expectEqualStrings("[57442;1:3u", actual[1..]); +} + test "kitty: delete" { var buf: [128]u8 = undefined; {