macos: proper event when sided mod released with other side pressed

Related to https://github.com/mitchellh/ghostty/issues/1082

This fixes two separate issues to follow along with the new spec changes
Kovid pushed to Kitty:

  1. When two modifiers are pressed and one is released, this shows up
     as a proper release event with the correct side. Previously, the
     correct side was shown but as a press event.

  2. When two modifiers are pressed and one is released, the Kitty event
     should not have that specific modifier set. For example, pressing
     left ctrl, then right ctrl, then releasing right ctrl should encode
     as "right ctrl released" but with NO modifiers still present.
This commit is contained in:
Mitchell Hashimoto
2023-12-13 21:33:39 -08:00
parent 9f7e53620b
commit 9642dd3275
2 changed files with 71 additions and 9 deletions

View File

@ -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)
}

View File

@ -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;
{