From bd647035e97da4aadfe1003877ecf64a3a655059 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sacha=20Tr=C3=A9moureux?= Date: Sat, 15 Aug 2026 12:48:37 +0200 Subject: [PATCH] input: don't emit fallback text on key release Key events without a kitty entry fall back to writing their UTF-8 text directly. On GTK, keys whose unshifted keysym is a dead key or level 5 latch have no unshifted codepoint and take this path. With event type reporting enabled, releases therefore emitted the same text as presses and duplicated characters in applications such as Neovim. Skip the raw text fallback for release events while retaining it for presses and repeats. Keep the guard in the shared encoder so release events for identified keys still retain the UTF-8 data used to derive alternate keys. Cover releases with and without report-all mode, and verify that repeat events continue to emit fallback text. --- src/input/key_encode.zig | 52 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/src/input/key_encode.zig b/src/input/key_encode.zig index 6ab5a4cc8..fd388452d 100644 --- a/src/input/key_encode.zig +++ b/src/input/key_encode.zig @@ -224,9 +224,11 @@ fn kitty( const entry = entry_ orelse { // No entry found. If we have UTF-8 text this is a pure text event // (e.g. composed/IME text), so send it as-is so programs can - // still receive it. - if (event.utf8.len > 0) return try writer.writeAll(event.utf8); - return; + // still receive it. Release events never insert text, same as the + // plain-text path above. + if (event.action == .release) return; + if (event.utf8.len == 0) return; + return try writer.writeAll(event.utf8); }; // If this is just a modifier we require "report all" to send the sequence. @@ -1492,6 +1494,50 @@ test "kitty: composed text with report all" { try testing.expectEqualStrings("\xc3\xbb", writer.buffered()); } +// A key whose base (unshifted) keysym is a dead key has no unshifted +// codepoint, so it has no kitty entry and falls back to writing the text +// directly. That must not happen on release or the text is inserted twice. +test "kitty: text fallback on release" { + for ([_]bool{ false, true }) |report_all| { + var buf: [128]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .action = .release, + .key = .unidentified, + .mods = .{ .shift = true }, + .utf8 = "!", + }, .{ + .kitty_flags = .{ + .disambiguate = true, + .report_events = true, + .report_alternates = true, + .report_all = report_all, + .report_associated = true, + }, + }); + try testing.expectEqualStrings("", writer.buffered()); + } +} + +test "kitty: text fallback on repeat" { + var buf: [128]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .action = .repeat, + .key = .unidentified, + .mods = .{ .shift = true }, + .utf8 = "!", + }, .{ + .kitty_flags = .{ + .disambiguate = true, + .report_events = true, + .report_alternates = true, + .report_associated = true, + }, + }); + try testing.expectEqualStrings("!", writer.buffered()); +} + test "kitty: shift+a on US keyboard" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf);