mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-18 01:36:08 +03:00
macOS: fix invalid kitty keyboard encoding of control characters (#5747)
Fixes #5743 This fixes a terrible regression where by fixing one issue we introduced another, and the other is that ctrl keys didn't work with programs with Kitty keyboard protocol. The problem is that our fix blindly assumed control was always consumed for translation, which is obviously wrong but I didn't think there'd be downstream effects. The reality is that we need to be more accurate. This PR makes it so that: * If macOS provides us with UTF-8 text, we assume all mods were involved except super * If macOS does not provide us with UTF-8 text, we use whatever UCKeyTranslate consumed * We never allow UCKeyTranslate to consume control because it turns it into the masked ASCII value and we don't want that. The only _new_ behavior which fixes the bug is point 2 above. Tested: 1. Dvorak Ctrl characters 2. Ergo-L Ctrl characters 3. US standard Ctrl characters 4. Japanese IME input Ctrl input to modify IME state 5. Ctrl keybindings with Kitty keyboard protocol in both Kitty and Neovim
This commit is contained in:
@ -224,6 +224,7 @@ pub const App = struct {
|
|||||||
const result: input.Keymap.Translation = if (event_text) |text| .{
|
const result: input.Keymap.Translation = if (event_text) |text| .{
|
||||||
.text = text,
|
.text = text,
|
||||||
.composing = event.composing,
|
.composing = event.composing,
|
||||||
|
.mods = translate_mods,
|
||||||
} else try self.keymap.translate(
|
} else try self.keymap.translate(
|
||||||
&buf,
|
&buf,
|
||||||
switch (target) {
|
switch (target) {
|
||||||
@ -231,14 +232,7 @@ pub const App = struct {
|
|||||||
.surface => |surface| &surface.keymap_state,
|
.surface => |surface| &surface.keymap_state,
|
||||||
},
|
},
|
||||||
@intCast(keycode),
|
@intCast(keycode),
|
||||||
if (comptime builtin.target.isDarwin()) mods: {
|
translate_mods,
|
||||||
// On macOS we strip ctrl because UCKeyTranslate
|
|
||||||
// converts to the masked values (i.e. ctrl+c becomes 3)
|
|
||||||
// and we don't want that behavior.
|
|
||||||
var v = translate_mods;
|
|
||||||
v.ctrl = false;
|
|
||||||
break :mods v;
|
|
||||||
} else translate_mods,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO(mitchellh): I think we can get rid of the above keymap
|
// TODO(mitchellh): I think we can get rid of the above keymap
|
||||||
@ -275,16 +269,12 @@ pub const App = struct {
|
|||||||
// then we clear the text. We handle non-printables in the
|
// then we clear the text. We handle non-printables in the
|
||||||
// key encoder manual (such as tab, ctrl+c, etc.)
|
// key encoder manual (such as tab, ctrl+c, etc.)
|
||||||
if (result.text.len == 1 and result.text[0] < 0x20) {
|
if (result.text.len == 1 and result.text[0] < 0x20) {
|
||||||
break :translate .{ .composing = false, .text = "" };
|
break :translate .{};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
break :translate result;
|
break :translate result;
|
||||||
} else .{ .composing = false, .text = "" };
|
} else .{};
|
||||||
|
|
||||||
// UCKeyTranslate always consumes all mods, so if we have any output
|
|
||||||
// then we've consumed our translate mods.
|
|
||||||
const consumed_mods: input.Mods = if (result.text.len > 0) translate_mods else .{};
|
|
||||||
|
|
||||||
// We need to always do a translation with no modifiers at all in
|
// We need to always do a translation with no modifiers at all in
|
||||||
// order to get the "unshifted_codepoint" for the key event.
|
// order to get the "unshifted_codepoint" for the key event.
|
||||||
@ -356,7 +346,7 @@ pub const App = struct {
|
|||||||
.key = key,
|
.key = key,
|
||||||
.physical_key = physical_key,
|
.physical_key = physical_key,
|
||||||
.mods = mods,
|
.mods = mods,
|
||||||
.consumed_mods = consumed_mods,
|
.consumed_mods = result.mods,
|
||||||
.composing = result.composing,
|
.composing = result.composing,
|
||||||
.utf8 = result.text,
|
.utf8 = result.text,
|
||||||
.unshifted_codepoint = unshifted_codepoint,
|
.unshifted_codepoint = unshifted_codepoint,
|
||||||
|
@ -50,10 +50,13 @@ pub const State = struct {
|
|||||||
pub const Translation = struct {
|
pub const Translation = struct {
|
||||||
/// The translation result. If this is a dead key state, then this will
|
/// The translation result. If this is a dead key state, then this will
|
||||||
/// be pre-edit text that can be displayed but will ultimately be replaced.
|
/// be pre-edit text that can be displayed but will ultimately be replaced.
|
||||||
text: []const u8,
|
text: []const u8 = "",
|
||||||
|
|
||||||
/// Whether the text is still composing, i.e. this is a dead key state.
|
/// Whether the text is still composing, i.e. this is a dead key state.
|
||||||
composing: bool,
|
composing: bool = false,
|
||||||
|
|
||||||
|
/// The mods that were consumed to produce this translation
|
||||||
|
mods: Mods = .{},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn init() !Keymap {
|
pub fn init() !Keymap {
|
||||||
@ -122,8 +125,18 @@ pub fn translate(
|
|||||||
out: []u8,
|
out: []u8,
|
||||||
state: *State,
|
state: *State,
|
||||||
code: u16,
|
code: u16,
|
||||||
mods: Mods,
|
input_mods: Mods,
|
||||||
) !Translation {
|
) !Translation {
|
||||||
|
// On macOS we strip ctrl because UCKeyTranslate
|
||||||
|
// converts to the masked values (i.e. ctrl+c becomes 3)
|
||||||
|
// and we don't want that behavior in Ghostty ever. This makes
|
||||||
|
// this file not a general-purpose keymap implementation.
|
||||||
|
const mods: Mods = mods: {
|
||||||
|
var v = input_mods;
|
||||||
|
v.ctrl = false;
|
||||||
|
break :mods v;
|
||||||
|
};
|
||||||
|
|
||||||
// Get the keycode for the space key, using comptime.
|
// Get the keycode for the space key, using comptime.
|
||||||
const code_space: u16 = comptime space: for (codes) |entry| {
|
const code_space: u16 = comptime space: for (codes) |entry| {
|
||||||
if (std.mem.eql(u8, entry.code, "Space"))
|
if (std.mem.eql(u8, entry.code, "Space"))
|
||||||
@ -183,7 +196,11 @@ pub fn translate(
|
|||||||
|
|
||||||
// Convert the utf16 to utf8
|
// Convert the utf16 to utf8
|
||||||
const len = try std.unicode.utf16leToUtf8(out, char[0..char_count]);
|
const len = try std.unicode.utf16leToUtf8(out, char[0..char_count]);
|
||||||
return .{ .text = out[0..len], .composing = composing };
|
return .{
|
||||||
|
.text = out[0..len],
|
||||||
|
.composing = composing,
|
||||||
|
.mods = mods,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Map to the modifiers format used by the UCKeyTranslate function.
|
/// Map to the modifiers format used by the UCKeyTranslate function.
|
||||||
|
@ -6,8 +6,9 @@ const Mods = @import("key.zig").Mods;
|
|||||||
|
|
||||||
pub const State = struct {};
|
pub const State = struct {};
|
||||||
pub const Translation = struct {
|
pub const Translation = struct {
|
||||||
text: []const u8,
|
text: []const u8 = "",
|
||||||
composing: bool,
|
composing: bool = false,
|
||||||
|
mods: Mods = .{},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn init() !KeymapNoop {
|
pub fn init() !KeymapNoop {
|
||||||
|
Reference in New Issue
Block a user