From 759ae94f15cdda8d13787f88f92153b00aa13c18 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 15 Aug 2023 22:38:54 -0700 Subject: [PATCH 01/20] macos: do not send ctrl or super into UCKeyTranslate see comment --- src/apprt/embedded.zig | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 2612022f5..83432a9dd 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -405,6 +405,17 @@ pub const Surface = struct { }, } + // 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. + // + // We also strip super because its not used for translation + // on macos and it results in a bad translation. + if (comptime builtin.target.isDarwin()) { + translate_mods.ctrl = false; + translate_mods.super = false; + } + break :translate_mods translate_mods; }; From 01282d3d15b0a5d3ca30214c47d050de89805e1a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 16 Aug 2023 07:50:45 -0700 Subject: [PATCH 02/20] core: process fixterms sequences for modified unicode characters --- src/Surface.zig | 21 ++++++++++++++ src/terminal/csi_u.zig | 64 ++++++++++++++++++++++++++++++++++++++++++ src/terminal/main.zig | 1 + 3 files changed, 86 insertions(+) create mode 100644 src/terminal/csi_u.zig diff --git a/src/Surface.zig b/src/Surface.zig index e002d374e..9d7c58b75 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1081,6 +1081,27 @@ pub fn charCallback( } } + // Let's see if we should apply fixterms to this codepoint. + // At this stage of key processing, we only need to apply fixterms + // to unicode codepoints (the point of charCallback) if we have + // ctrl set. + if (mods.ctrl) { + const csi_u_mods = terminal.csi_u.Mods.fromInput(mods); + const resp = try std.fmt.bufPrint( + &data, + "\x1B[{};{}u", + .{ codepoint, csi_u_mods.seqInt() }, + ); + _ = self.io_thread.mailbox.push(.{ + .write_small = .{ + .data = data, + .len = @intCast(resp.len), + }, + }, .{ .forever = {} }); + try self.io_thread.wakeup.notify(); + return; + } + // Prefix our data with ESC if we have alt pressed. var i: u8 = 0; if (mods.alt) alt: { diff --git a/src/terminal/csi_u.zig b/src/terminal/csi_u.zig new file mode 100644 index 000000000..93b1d1e7d --- /dev/null +++ b/src/terminal/csi_u.zig @@ -0,0 +1,64 @@ +//! This file has information related to Paul Evans's "fixterms" +//! encoding, also sometimes referred to as "CSI u" encoding. +//! +//! https://www.leonerd.org.uk/hacks/fixterms/ + +const std = @import("std"); + +const input = @import("../input.zig"); + +pub const Mods = packed struct(u3) { + shift: bool = false, + alt: bool = false, + ctrl: bool = false, + + /// Convert an input mods value into the CSI u mods value. + pub fn fromInput(mods: input.Mods) Mods { + return .{ + .shift = mods.shift, + .alt = mods.alt, + .ctrl = mods.ctrl, + }; + } + + /// Returns the raw int value of this packed struct. + pub fn int(self: Mods) u3 { + return @bitCast(self); + } + + /// Returns the integer value sent as part of the CSI u sequence. + /// This adds 1 to the bitmask value as described in the spec. + pub fn seqInt(self: Mods) u4 { + const raw: u4 = @intCast(self.int()); + return raw + 1; + } +}; + +test "modifer sequence values" { + // This is all sort of trivially seen by looking at the code but + // we want to make sure we never regress this. + const testing = std.testing; + var mods: Mods = .{}; + try testing.expectEqual(@as(u4, 1), mods.seqInt()); + + mods = .{ .shift = true }; + try testing.expectEqual(@as(u4, 2), mods.seqInt()); + + mods = .{ .alt = true }; + try testing.expectEqual(@as(u4, 3), mods.seqInt()); + + mods = .{ .ctrl = true }; + try testing.expectEqual(@as(u4, 5), mods.seqInt()); + + mods = .{ .alt = true, .shift = true }; + try testing.expectEqual(@as(u4, 4), mods.seqInt()); + + mods = .{ .ctrl = true, .shift = true }; + try testing.expectEqual(@as(u4, 6), mods.seqInt()); + + mods = .{ .alt = true, .ctrl = true }; + try testing.expectEqual(@as(u4, 7), mods.seqInt()); + + mods = .{ .alt = true, .ctrl = true, .shift = true }; + try testing.expectEqual(@as(u4, 8), mods.seqInt()); +} diff --git a/src/terminal/main.zig b/src/terminal/main.zig index 3de537656..f08c97326 100644 --- a/src/terminal/main.zig +++ b/src/terminal/main.zig @@ -7,6 +7,7 @@ const csi = @import("csi.zig"); const sgr = @import("sgr.zig"); pub const point = @import("point.zig"); pub const color = @import("color.zig"); +pub const csi_u = @import("csi_u.zig"); pub const modes = @import("modes.zig"); pub const parse_table = @import("parse_table.zig"); From 62081a51b0c05744fb27a154c5406c2db783190c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 16 Aug 2023 08:12:07 -0700 Subject: [PATCH 03/20] core: add KeyEvent --- src/Surface.zig | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/Surface.zig b/src/Surface.zig index 9d7c58b75..687faad00 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1139,6 +1139,40 @@ pub fn charCallback( try self.io_thread.wakeup.notify(); } +/// A key input event. +pub const KeyEvent = struct { + /// The action: press, release, etc. + action: input.Action, + + /// "key" is the logical key that was pressed. For example, if + /// a Dvorak keyboard layout is being used on a US keyboard, + /// the "i" physical key will be reported as "c". The physical + /// key is the key that was physically pressed on the keyboard. + key: input.Key, + physical_key: input.Key, + + /// Mods are the modifiers that are pressed. + mods: input.Mods, + + /// The mods that were consumed in order to generate the text + /// in utf8. This has the mods set that were consumed, so to + /// get the set of mods that are effective you must negate + /// mods with this. + /// + /// This field is meaningless if utf8 is empty. + consumed_mods: input.Mods, + + /// Composing is true when this key event is part of a dead key + /// composition sequence and we're in the middle of it. + composing: bool, + + /// The utf8 sequence that was generated by this key event. + /// This will be an empty string if there is no text generated. + /// If composing is true and this is non-empty, this is preedit + /// text. + utf8: []const u8, +}; + /// Called for a single key event. /// /// This will return true if the key was handled/consumed. In that case, From f57fd99d3e0d8aa650c5926a4ed27251b7d25c21 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 16 Aug 2023 08:52:53 -0700 Subject: [PATCH 04/20] input: starting to work on KeyEncoder, got ctrl sequences --- src/input.zig | 1 + src/input/KeyEncoder.zig | 131 +++++++++++++++++++++++++++++++++++++++ src/input/key.zig | 56 ++++++++++++++++- 3 files changed, 186 insertions(+), 2 deletions(-) create mode 100644 src/input/KeyEncoder.zig diff --git a/src/input.zig b/src/input.zig index e2deed0e2..f382386c6 100644 --- a/src/input.zig +++ b/src/input.zig @@ -6,6 +6,7 @@ pub usingnamespace @import("input/key.zig"); pub const function_keys = @import("input/function_keys.zig"); pub const keycodes = @import("input/keycodes.zig"); pub const Binding = @import("input/Binding.zig"); +pub const KeyEncoder = @import("input/KeyEncoder.zig"); pub const SplitDirection = Binding.Action.SplitDirection; pub const SplitFocusDirection = Binding.Action.SplitFocusDirection; diff --git a/src/input/KeyEncoder.zig b/src/input/KeyEncoder.zig new file mode 100644 index 000000000..65a80c1f7 --- /dev/null +++ b/src/input/KeyEncoder.zig @@ -0,0 +1,131 @@ +/// KeyEncoder is responsible for processing keyboard input and generating +/// the proper VT sequence for any events. +/// +/// A new KeyEncoder should be created for each individual key press. +/// These encoders are not meant to be reused. +const KeyEncoder = @This(); + +const std = @import("std"); +const testing = std.testing; + +const key = @import("key.zig"); + +key: key.Key, +binding_mods: key.Mods, + +/// Initialize +fn init(event: key.Event) KeyEncoder { + const effective_mods = event.effectiveMods(); + const binding_mods = effective_mods.binding(); + + return .{ + .key = event.key, + .binding_mods = binding_mods, + }; +} + +/// Returns the C0 byte for the key event if it should be used. +/// This converts a key event into the expected terminal behavior +/// such as Ctrl+C turning into 0x03, amongst many other translations. +/// +/// This will return null if the key event should not be converted +/// into a C0 byte. There are many cases for this and you should read +/// the source code to understand them. +fn ctrlSeq(self: *const KeyEncoder) ?u8 { + // Remove alt from our modifiers because it does not impact whether + // we are generating a ctrl sequence. + const unalt_mods = unalt_mods: { + var unalt_mods = self.binding_mods; + unalt_mods.alt = false; + break :unalt_mods unalt_mods; + }; + + // If we have any other modifier key set, then we do not generate + // a C0 sequence. + const ctrl_only = comptime (key.Mods{ .ctrl = true }).int(); + if (unalt_mods.int() != ctrl_only) return null; + + // The normal approach to get this value is to make the ascii byte + // with 0x1F. However, not all apprt key translation will properly + // generate the correct value so we just hardcode this based on + // logical key. + return switch (self.key) { + .space => 0, + .slash => 0x1F, + .zero => 0x30, + .one => 0x31, + .two => 0x00, + .three => 0x1B, + .four => 0x1C, + .five => 0x1D, + .six => 0x1E, + .seven => 0x1F, + .eight => 0x7F, + .nine => 0x39, + .backslash => 0x1C, + .left_bracket => 0x1B, + .right_bracket => 0x1D, + .a => 0x01, + .b => 0x02, + .c => 0x03, + .d => 0x04, + .e => 0x05, + .f => 0x06, + .g => 0x07, + .h => 0x08, + .i => 0x09, + .j => 0x0A, + .k => 0x0B, + .l => 0x0C, + .m => 0x0D, + .n => 0x0E, + .o => 0x0F, + .p => 0x10, + .q => 0x11, + .r => 0x12, + .s => 0x13, + .t => 0x14, + .u => 0x15, + .v => 0x16, + .w => 0x17, + .x => 0x18, + .y => 0x19, + .z => 0x1A, + else => null, + }; +} + +test "ctrlseq: normal ctrl c" { + const enc = init(.{ .key = .c, .mods = .{ .ctrl = true } }); + const seq = enc.ctrlSeq(); + try testing.expectEqual(@as(u8, 0x03), seq.?); +} + +test "ctrlseq: alt should be allowed" { + const enc = init(.{ .key = .c, .mods = .{ .alt = true, .ctrl = true } }); + const seq = enc.ctrlSeq(); + try testing.expectEqual(@as(u8, 0x03), seq.?); +} + +test "ctrlseq: no ctrl does nothing" { + const enc = init(.{ .key = .c, .mods = .{} }); + try testing.expect(enc.ctrlSeq() == null); +} + +test "ctrlseq: shift does not generate ctrl seq" { + { + const enc = init(.{ + .key = .c, + .mods = .{ .shift = true }, + }); + try testing.expect(enc.ctrlSeq() == null); + } + + { + const enc = init(.{ + .key = .c, + .mods = .{ .shift = true, .ctrl = true }, + }); + try testing.expect(enc.ctrlSeq() == null); + } +} diff --git a/src/input/key.zig b/src/input/key.zig index 8ec485c9a..92db8009e 100644 --- a/src/input/key.zig +++ b/src/input/key.zig @@ -1,8 +1,55 @@ const std = @import("std"); const Allocator = std.mem.Allocator; -/// A bitmask for all key modifiers. This is taken directly from the -/// GLFW representation, but we use this generically. +/// A generic key input event. This is the information that is necessary +/// regardless of apprt in order to generate the proper terminal +/// control sequences for a given key press. +/// +/// Some apprts may not be able to provide all of this information, such +/// as GLFW. In this case, the apprt should provide as much information +/// as it can and it should be expected that the terminal behavior +/// will not be totally correct. +pub const Event = struct { + /// The action: press, release, etc. + action: Action = .press, + + /// "key" is the logical key that was pressed. For example, if + /// a Dvorak keyboard layout is being used on a US keyboard, + /// the "i" physical key will be reported as "c". The physical + /// key is the key that was physically pressed on the keyboard. + key: Key, + physical_key: Key = .invalid, + + /// Mods are the modifiers that are pressed. + mods: Mods = .{}, + + /// The mods that were consumed in order to generate the text + /// in utf8. This has the mods set that were consumed, so to + /// get the set of mods that are effective you must negate + /// mods with this. + /// + /// This field is meaningless if utf8 is empty. + consumed_mods: Mods = .{}, + + /// Composing is true when this key event is part of a dead key + /// composition sequence and we're in the middle of it. + composing: bool = false, + + /// The utf8 sequence that was generated by this key event. + /// This will be an empty string if there is no text generated. + /// If composing is true and this is non-empty, this is preedit + /// text. + utf8: []const u8 = "", + + /// Returns the effective modifiers for this event. The effective + /// modifiers are the mods that should be considered for keybindings. + pub fn effectiveMods(self: Event) Mods { + if (self.utf8.len == 0) return self.mods; + return self.mods.unset(self.consumed_mods); + } +}; + +/// A bitmask for all key modifiers. /// /// IMPORTANT: Any changes here update include/ghostty.h pub const Mods = packed struct(Mods.Backing) { @@ -56,6 +103,11 @@ pub const Mods = packed struct(Mods.Backing) { }; } + /// Perform `self &~ other` to remove the other mods from self. + pub fn unset(self: Mods, other: Mods) Mods { + return @bitCast(self.int() & ~other.int()); + } + /// Returns the mods without locks set. pub fn withoutLocks(self: Mods) Mods { var copy = self; From 6555bb1410da29e5ce143ec554dd78b0bd2dbdc2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 16 Aug 2023 09:30:36 -0700 Subject: [PATCH 05/20] input: KeyEncoder legacy encoding handles old keyCallback logic --- src/input/KeyEncoder.zig | 185 ++++++++++++++++++++++++++++++++------- 1 file changed, 152 insertions(+), 33 deletions(-) diff --git a/src/input/KeyEncoder.zig b/src/input/KeyEncoder.zig index 65a80c1f7..380f6b090 100644 --- a/src/input/KeyEncoder.zig +++ b/src/input/KeyEncoder.zig @@ -9,19 +9,102 @@ const std = @import("std"); const testing = std.testing; const key = @import("key.zig"); +const function_keys = @import("function_keys.zig"); -key: key.Key, -binding_mods: key.Mods, +event: key.Event, -/// Initialize -fn init(event: key.Event) KeyEncoder { - const effective_mods = event.effectiveMods(); +/// The state of various modes of a terminal that impact encoding. +cursor_key_application: bool = false, +keypad_key_application: bool = false, +modify_other_keys_state_2: bool = false, + +/// Perform legacy encoding of the key event. "Legacy" in this case +/// is referring to the behavior of traditional terminals, plus +/// xterm's `modifyOtherKeys`, plus Paul Evans's "fixterms" spec. +/// These together combine the legacy protocol because they're all +/// meant to be extensions that do not change any existing behavior +/// and therefore safe to combine. +fn legacy( + self: *const KeyEncoder, + buf: []u8, +) ![]const u8 { + const effective_mods = self.event.effectiveMods(); const binding_mods = effective_mods.binding(); - return .{ - .key = event.key, - .binding_mods = binding_mods, - }; + // Legacy encoding only does press/repeat + if (self.event.action != .press and + self.event.action != .repeat) return ""; + + // If we match a PC style function key then that is our result. + if (pcStyleFunctionKey( + self.event.key, + binding_mods, + self.cursor_key_application, + self.keypad_key_application, + self.modify_other_keys_state_2, + )) |sequence| return sequence; + + // If we match a control sequence, we output that directly. + if (ctrlSeq(self.event.key, binding_mods)) |char| { + // C0 sequences support alt-as-esc prefixing. + if (binding_mods.alt) { + if (buf.len < 2) return error.OutOfMemory; + buf[0] = 0x1B; + buf[1] = char; + return buf[0..2]; + } + + if (buf.len < 1) return error.OutOfMemory; + buf[0] = char; + return buf[0..1]; + } + + return ""; +} + +/// Determines whether the key should be encoded in the xterm +/// "PC-style Function Key" syntax (roughly). This is a hardcoded +/// table of keys and modifiers that result in a specific sequence. +fn pcStyleFunctionKey( + keyval: key.Key, + mods: key.Mods, + cursor_key_application: bool, + keypad_key_application: bool, + modify_other_keys: bool, // True if state 2 +) ?[]const u8 { + const mods_int = mods.int(); + for (function_keys.keys.get(keyval)) |entry| { + switch (entry.cursor) { + .any => {}, + .normal => if (cursor_key_application) continue, + .application => if (!cursor_key_application) continue, + } + + switch (entry.keypad) { + .any => {}, + .normal => if (keypad_key_application) continue, + .application => if (!keypad_key_application) continue, + } + + switch (entry.modify_other_keys) { + .any => {}, + .set => if (modify_other_keys) continue, + .set_other => if (!modify_other_keys) continue, + } + + const entry_mods_int = entry.mods.int(); + if (entry_mods_int == 0) { + if (mods_int != 0 and !entry.mods_empty_is_any) continue; + // mods are either empty, or empty means any so we allow it. + } else if (entry_mods_int != mods_int) { + // any set mods require an exact match + continue; + } + + return entry.sequence; + } + + return null; } /// Returns the C0 byte for the key event if it should be used. @@ -31,11 +114,11 @@ fn init(event: key.Event) KeyEncoder { /// This will return null if the key event should not be converted /// into a C0 byte. There are many cases for this and you should read /// the source code to understand them. -fn ctrlSeq(self: *const KeyEncoder) ?u8 { +fn ctrlSeq(keyval: key.Key, mods: key.Mods) ?u8 { // Remove alt from our modifiers because it does not impact whether // we are generating a ctrl sequence. const unalt_mods = unalt_mods: { - var unalt_mods = self.binding_mods; + var unalt_mods = mods; unalt_mods.alt = false; break :unalt_mods unalt_mods; }; @@ -49,7 +132,7 @@ fn ctrlSeq(self: *const KeyEncoder) ?u8 { // with 0x1F. However, not all apprt key translation will properly // generate the correct value so we just hardcode this based on // logical key. - return switch (self.key) { + return switch (keyval) { .space => 0, .slash => 0x1F, .zero => 0x30, @@ -95,37 +178,73 @@ fn ctrlSeq(self: *const KeyEncoder) ?u8 { }; } +test "legacy: ctrl+alt+c" { + var buf: [128]u8 = undefined; + var enc: KeyEncoder = .{ + .event = .{ + .key = .c, + .mods = .{ .ctrl = true, .alt = true }, + }, + }; + + const actual = try enc.legacy(&buf); + try testing.expectEqualStrings("\x1b\x03", actual); +} + +test "legacy: ctrl+c" { + var buf: [128]u8 = undefined; + var enc: KeyEncoder = .{ + .event = .{ + .key = .c, + .mods = .{ .ctrl = true }, + }, + }; + + const actual = try enc.legacy(&buf); + try testing.expectEqualStrings("\x03", actual); +} + +test "legacy: ctrl+space" { + var buf: [128]u8 = undefined; + var enc: KeyEncoder = .{ + .event = .{ + .key = .space, + .mods = .{ .ctrl = true }, + }, + }; + + const actual = try enc.legacy(&buf); + try testing.expectEqualStrings("\x00", actual); +} + +test "legacy: ctrl+shift+backspace" { + var buf: [128]u8 = undefined; + var enc: KeyEncoder = .{ + .event = .{ + .key = .backspace, + .mods = .{ .ctrl = true, .shift = true }, + }, + }; + + const actual = try enc.legacy(&buf); + try testing.expectEqualStrings("\x08", actual); +} + test "ctrlseq: normal ctrl c" { - const enc = init(.{ .key = .c, .mods = .{ .ctrl = true } }); - const seq = enc.ctrlSeq(); + const seq = ctrlSeq(.c, .{ .ctrl = true }); try testing.expectEqual(@as(u8, 0x03), seq.?); } test "ctrlseq: alt should be allowed" { - const enc = init(.{ .key = .c, .mods = .{ .alt = true, .ctrl = true } }); - const seq = enc.ctrlSeq(); + const seq = ctrlSeq(.c, .{ .alt = true, .ctrl = true }); try testing.expectEqual(@as(u8, 0x03), seq.?); } test "ctrlseq: no ctrl does nothing" { - const enc = init(.{ .key = .c, .mods = .{} }); - try testing.expect(enc.ctrlSeq() == null); + try testing.expect(ctrlSeq(.c, .{}) == null); } test "ctrlseq: shift does not generate ctrl seq" { - { - const enc = init(.{ - .key = .c, - .mods = .{ .shift = true }, - }); - try testing.expect(enc.ctrlSeq() == null); - } - - { - const enc = init(.{ - .key = .c, - .mods = .{ .shift = true, .ctrl = true }, - }); - try testing.expect(enc.ctrlSeq() == null); - } + try testing.expect(ctrlSeq(.c, .{ .shift = true }) == null); + try testing.expect(ctrlSeq(.c, .{ .shift = true, .ctrl = true }) == null); } From 871f797758db6b27e95ef9e4391990033c623d87 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 16 Aug 2023 09:51:33 -0700 Subject: [PATCH 06/20] input: KeyEncoder handles the charCallback stuff now too --- src/input/KeyEncoder.zig | 70 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/src/input/KeyEncoder.zig b/src/input/KeyEncoder.zig index 380f6b090..4af8d5e6d 100644 --- a/src/input/KeyEncoder.zig +++ b/src/input/KeyEncoder.zig @@ -14,6 +14,7 @@ const function_keys = @import("function_keys.zig"); event: key.Event, /// The state of various modes of a terminal that impact encoding. +alt_esc_prefix: bool = false, cursor_key_application: bool = false, keypad_key_application: bool = false, modify_other_keys_state_2: bool = false, @@ -59,7 +60,59 @@ fn legacy( return buf[0..1]; } - return ""; + // If we have no UTF8 text then at this point there is nothing to do. + const utf8 = self.event.utf8; + if (utf8.len == 0) return ""; + + // In modify other keys state 2, we send the CSI 27 sequence + // for any char with a modifier. Ctrl sequences like Ctrl+a + // are already handled above. + if (self.modify_other_keys_state_2) modify_other: { + const view = try std.unicode.Utf8View.init(utf8); + var it = view.iterator(); + const codepoint = it.nextCodepoint() orelse break :modify_other; + + // We only do this if we have a single codepoint. There shouldn't + // ever be a multi-codepoint sequence that triggers this. + if (it.nextCodepoint() != null) break :modify_other; + + // This copies xterm's `ModifyOtherKeys` function that returns + // whether modify other keys should be encoded for the given + // input. + const should_modify = should_modify: { + // xterm IsControlInput + if (codepoint >= 0x40 and codepoint <= 0x7F) + break :should_modify true; + + // If we have anything other than shift pressed, encode. + var mods_no_shift = binding_mods; + mods_no_shift.shift = false; + if (!mods_no_shift.empty()) break :should_modify true; + + // We only have shift pressed. We only allow space. + if (codepoint == ' ') break :should_modify true; + + // This logic isn't complete but I don't fully understand + // the rest so I'm going to wait until we can have a + // reasonable test scenario. + break :should_modify false; + }; + + if (should_modify) { + for (function_keys.modifiers, 2..) |modset, code| { + if (!binding_mods.equal(modset)) continue; + return try std.fmt.bufPrint( + buf, + "\x1B[27;{};{}~", + .{ code, codepoint }, + ); + } + } + } + + // TODO: alt-prefix utf8 + + return utf8; } /// Determines whether the key should be encoded in the xterm @@ -230,6 +283,21 @@ test "legacy: ctrl+shift+backspace" { try testing.expectEqualStrings("\x08", actual); } +test "legacy: ctrl+shift+char with modify other state 2" { + var buf: [128]u8 = undefined; + var enc: KeyEncoder = .{ + .event = .{ + .key = .h, + .mods = .{ .ctrl = true, .shift = true }, + .utf8 = "H", + }, + .modify_other_keys_state_2 = true, + }; + + const actual = try enc.legacy(&buf); + try testing.expectEqualStrings("\x1b[27;6;72~", actual); +} + test "ctrlseq: normal ctrl c" { const seq = ctrlSeq(.c, .{ .ctrl = true }); try testing.expectEqual(@as(u8, 0x03), seq.?); From a2d4e7ce2905ef371c9aa001238eca67b6ef63f3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 16 Aug 2023 10:12:14 -0700 Subject: [PATCH 07/20] input: legacy key encoding handles alt-as-esc for general utf8 --- src/input/KeyEncoder.zig | 41 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/input/KeyEncoder.zig b/src/input/KeyEncoder.zig index 4af8d5e6d..a49e9f791 100644 --- a/src/input/KeyEncoder.zig +++ b/src/input/KeyEncoder.zig @@ -110,7 +110,46 @@ fn legacy( } } - // TODO: alt-prefix utf8 + // // Let's see if we should apply fixterms to this codepoint. + // // At this stage of key processing, we only need to apply fixterms + // // to unicode codepoints (the point of charCallback) if we have + // // ctrl set. + // if (mods.ctrl) { + // const csi_u_mods = terminal.csi_u.Mods.fromInput(mods); + // const resp = try std.fmt.bufPrint( + // &data, + // "\x1B[{};{}u", + // .{ codepoint, csi_u_mods.seqInt() }, + // ); + // _ = self.io_thread.mailbox.push(.{ + // .write_small = .{ + // .data = data, + // .len = @intCast(resp.len), + // }, + // }, .{ .forever = {} }); + // try self.io_thread.wakeup.notify(); + // return; + // } + + // If we have alt-pressed and alt-esc-prefix is enabled, then + // we need to prefix the utf8 sequence with an esc. + if (binding_mods.alt and self.alt_esc_prefix) { + // TODO: port this, I think we can just use effective mods + // without any OS special case + // + // On macOS, we have to opt-in to using alt because option + // by default is a unicode character sequence. + // if (comptime builtin.target.isDarwin()) { + // switch (self.config.macos_option_as_alt) { + // .false => break :alt, + // .true => {}, + // .left => if (mods.sides.alt != .left) break :alt, + // .right => if (mods.sides.alt != .right) break :alt, + // } + // } + + return try std.fmt.bufPrint(buf, "\x1B{s}", .{utf8}); + } return utf8; } From 9b90692fc7fae58c58bd18bae3f0b3051e3c78d1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 16 Aug 2023 10:12:40 -0700 Subject: [PATCH 08/20] core: start on key2Callback for the new callback that uses KeyEncoder --- src/Surface.zig | 95 ++++++++++++++++++++++++++++------------ src/input/KeyEncoder.zig | 2 +- 2 files changed, 68 insertions(+), 29 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 687faad00..0f7ad2d61 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1139,39 +1139,78 @@ pub fn charCallback( try self.io_thread.wakeup.notify(); } -/// A key input event. -pub const KeyEvent = struct { - /// The action: press, release, etc. - action: input.Action, +pub fn key2Callback( + self: *Surface, + event: input.KeyEvent, +) !bool { + // Before encoding, we see if we have any keybindings for this + // key. Those always intercept before any encoding tasks. + if (event.action == .press or event.action == .repeat) { + const binding_mods = event.mods.effectiveMods().binding(); + const binding_action_: ?input.Binding.Action = action: { + var trigger: input.Binding.Trigger = .{ + .mods = binding_mods, + .key = event.key, + }; - /// "key" is the logical key that was pressed. For example, if - /// a Dvorak keyboard layout is being used on a US keyboard, - /// the "i" physical key will be reported as "c". The physical - /// key is the key that was physically pressed on the keyboard. - key: input.Key, - physical_key: input.Key, + const set = self.config.keybind.set; + if (set.get(trigger)) |v| break :action v; - /// Mods are the modifiers that are pressed. - mods: input.Mods, + trigger.key = event.physical_key; + trigger.physical = true; + if (set.get(trigger)) |v| break :action v; - /// The mods that were consumed in order to generate the text - /// in utf8. This has the mods set that were consumed, so to - /// get the set of mods that are effective you must negate - /// mods with this. - /// - /// This field is meaningless if utf8 is empty. - consumed_mods: input.Mods, + break :action null; + }; - /// Composing is true when this key event is part of a dead key - /// composition sequence and we're in the middle of it. - composing: bool, + if (binding_action_) |binding_action| { + //log.warn("BINDING ACTION={}", .{binding_action}); + try self.performBindingAction(binding_action); + return true; + } + } - /// The utf8 sequence that was generated by this key event. - /// This will be an empty string if there is no text generated. - /// If composing is true and this is non-empty, this is preedit - /// text. - utf8: []const u8, -}; + // No binding, so we have to perform an encoding task. This + // may still result in no encoding. Under different modes and + // inputs there are many keybindings that result in no encoding + // whatsoever. + const enc: input.KeyEncoder = enc: { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + const t = &self.io.terminal; + break :enc .{ + .event = event, + .alt_esc_prefix = t.modes.get(.alt_esc_prefix), + .cursor_key_application = t.modes.get(.cursor_keys), + .keypad_key_application = t.modes.get(.keypad_keys), + .modify_other_keys_state_2 = t.flags.modify_other_keys_2, + }; + }; + + var data: termio.Message.WriteReq.Small.Array = undefined; + const seq = try enc.legacy(&data); + if (seq.len == 0) return false; + + _ = self.io_thread.mailbox.push(.{ + .write_small = .{ + .data = data, + .len = seq.len, + }, + }, .{ .forever = {} }); + try self.io_thread.wakeup.notify(); + + // If we have a sequence to emit then we always want to clear the + // selection and scroll to the bottom. + { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + self.setSelection(null); + try self.io.terminal.scrollViewport(.{ .bottom = {} }); + try self.queueRender(); + } + + return true; +} /// Called for a single key event. /// diff --git a/src/input/KeyEncoder.zig b/src/input/KeyEncoder.zig index a49e9f791..48b015779 100644 --- a/src/input/KeyEncoder.zig +++ b/src/input/KeyEncoder.zig @@ -25,7 +25,7 @@ modify_other_keys_state_2: bool = false, /// These together combine the legacy protocol because they're all /// meant to be extensions that do not change any existing behavior /// and therefore safe to combine. -fn legacy( +pub fn legacy( self: *const KeyEncoder, buf: []u8, ) ![]const u8 { From aadb78394ba5f4fded0c94e4a448942ef0c7fc4f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 16 Aug 2023 10:14:43 -0700 Subject: [PATCH 09/20] input: legacy encoding never emits sequence during dead key state --- src/input/KeyEncoder.zig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/input/KeyEncoder.zig b/src/input/KeyEncoder.zig index 48b015779..0da31fd75 100644 --- a/src/input/KeyEncoder.zig +++ b/src/input/KeyEncoder.zig @@ -36,6 +36,9 @@ pub fn legacy( if (self.event.action != .press and self.event.action != .repeat) return ""; + // If we're in a dead key state then we never emit a sequence. + if (self.event.composing) return ""; + // If we match a PC style function key then that is our result. if (pcStyleFunctionKey( self.event.key, From c254a8b09e719f8d82c881562e9f19252a1f881f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 16 Aug 2023 10:36:22 -0700 Subject: [PATCH 10/20] input: encoding should always write to the buf --- src/input/KeyEncoder.zig | 14 +++++++++++--- src/input/key.zig | 4 ++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/input/KeyEncoder.zig b/src/input/KeyEncoder.zig index 0da31fd75..3ac8a5fc9 100644 --- a/src/input/KeyEncoder.zig +++ b/src/input/KeyEncoder.zig @@ -11,7 +11,7 @@ const testing = std.testing; const key = @import("key.zig"); const function_keys = @import("function_keys.zig"); -event: key.Event, +event: key.KeyEvent, /// The state of various modes of a terminal that impact encoding. alt_esc_prefix: bool = false, @@ -46,7 +46,7 @@ pub fn legacy( self.cursor_key_application, self.keypad_key_application, self.modify_other_keys_state_2, - )) |sequence| return sequence; + )) |sequence| return copyToBuf(buf, sequence); // If we match a control sequence, we output that directly. if (ctrlSeq(self.event.key, binding_mods)) |char| { @@ -154,7 +154,15 @@ pub fn legacy( return try std.fmt.bufPrint(buf, "\x1B{s}", .{utf8}); } - return utf8; + return try copyToBuf(buf, utf8); +} + +/// A helper to memcpy a src value to a buffer and return the result. +fn copyToBuf(buf: []u8, src: []const u8) ![]const u8 { + if (src.len > buf.len) return error.OutOfMemory; + const result = buf[0..src.len]; + @memcpy(result, src); + return result; } /// Determines whether the key should be encoded in the xterm diff --git a/src/input/key.zig b/src/input/key.zig index 92db8009e..622d50556 100644 --- a/src/input/key.zig +++ b/src/input/key.zig @@ -9,7 +9,7 @@ const Allocator = std.mem.Allocator; /// as GLFW. In this case, the apprt should provide as much information /// as it can and it should be expected that the terminal behavior /// will not be totally correct. -pub const Event = struct { +pub const KeyEvent = struct { /// The action: press, release, etc. action: Action = .press, @@ -43,7 +43,7 @@ pub const Event = struct { /// Returns the effective modifiers for this event. The effective /// modifiers are the mods that should be considered for keybindings. - pub fn effectiveMods(self: Event) Mods { + pub fn effectiveMods(self: KeyEvent) Mods { if (self.utf8.len == 0) return self.mods; return self.mods.unset(self.consumed_mods); } From 1a918bc64b66d7ab870b8904811b5f3db69d2126 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 16 Aug 2023 10:37:48 -0700 Subject: [PATCH 11/20] apprt/gtk: call new key2callback using the all-in-one key event --- src/Surface.zig | 6 ++-- src/apprt/gtk.zig | 80 +++++++++++++++++++---------------------------- 2 files changed, 37 insertions(+), 49 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 0f7ad2d61..779614641 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1143,10 +1143,12 @@ pub fn key2Callback( self: *Surface, event: input.KeyEvent, ) !bool { + // log.debug("keyCallback event={}", .{event}); + // Before encoding, we see if we have any keybindings for this // key. Those always intercept before any encoding tasks. if (event.action == .press or event.action == .repeat) { - const binding_mods = event.mods.effectiveMods().binding(); + const binding_mods = event.effectiveMods().binding(); const binding_action_: ?input.Binding.Action = action: { var trigger: input.Binding.Trigger = .{ .mods = binding_mods, @@ -1194,7 +1196,7 @@ pub fn key2Callback( _ = self.io_thread.mailbox.push(.{ .write_small = .{ .data = data, - .len = seq.len, + .len = @intCast(seq.len), }, }, .{ .forever = {} }); try self.io_thread.wakeup.notify(); diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index 3a949adef..8d03c6720 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -1213,10 +1213,12 @@ pub const Surface = struct { const event = c.gtk_event_controller_get_current_event(@ptrCast(ec_key)); _ = c.gtk_im_context_filter_keypress(self.im_context, event) != 0; - // If we aren't composing, then we set our preedit to empty no matter what. - if (!self.im_composing) { - self.core_surface.preeditCallback(null) catch {}; - } + // Get our consumed modifiers + const consumed_mods: input.Mods = consumed: { + const raw = c.gdk_key_event_get_consumed_modifiers(event); + const masked = raw & c.GDK_MODIFIER_MASK; + break :consumed translateMods(masked); + }; // If we're not in a dead key state, we want to translate our text // to some input.Key. @@ -1252,48 +1254,29 @@ pub const Surface = struct { // mods, // }); - // If both keys are invalid then we won't call the key callback. But - // if either one is valid, we want to give it a chance. - if (key != .invalid or physical_key != .invalid) { - const consumed = self.core_surface.keyCallback( - .press, - key, - physical_key, - mods, - ) catch |err| { - log.err("error in key callback err={}", .{err}); - return 0; - }; - - // If we consume the key then we want to reset the dead key state. - if (consumed) { - c.gtk_im_context_reset(self.im_context); - self.core_surface.preeditCallback(null) catch {}; - return 1; - } - } - // If this is a dead key, then we're composing a character and - // we end processing here. We don't process keybinds for dead keys. - if (self.im_composing) { + // we need to set our proper preedit state. + if (self.im_composing) preedit: { const text = self.im_buf[0..self.im_len]; const view = std.unicode.Utf8View.init(text) catch |err| { log.warn("cannot build utf8 view over input: {}", .{err}); - return 0; + break :preedit; }; var it = view.iterator(); const cp: u21 = it.nextCodepoint() orelse 0; self.core_surface.preeditCallback(cp) catch |err| { log.err("error in preedit callback err={}", .{err}); - return 0; + break :preedit; }; - - return 0; + } else { + // If we aren't composing, then we set our preedit to + // empty no matter what. + self.core_surface.preeditCallback(null) catch {}; } - // If we aren't composing and have no text, we try to convert the keyval - // to a text value. We have to do this because GTK will not process + // If we have no UTF-8 text, we try to convert our keyval to + // a text value. We have to do this because GTK will not process // "Ctrl+Shift+1" (on US keyboards) as "Ctrl+!" but instead as "". // But the keyval is set correctly so we can at least extract that. if (self.im_len == 0) { @@ -1307,21 +1290,24 @@ pub const Surface = struct { } } - // Next, we want to call the char callback with each codepoint. - if (self.im_len > 0) { - const text = self.im_buf[0..self.im_len]; - const view = std.unicode.Utf8View.init(text) catch |err| { - log.warn("cannot build utf8 view over input: {}", .{err}); - return 0; - }; - var it = view.iterator(); - while (it.nextCodepoint()) |cp| { - self.core_surface.charCallback(cp, mods) catch |err| { - log.err("error in char callback err={}", .{err}); - return 0; - }; - } + // Invoke the core Ghostty logic to handle this input. + const consumed = self.core_surface.key2Callback(.{ + .action = .press, + .key = key, + .physical_key = physical_key, + .mods = mods, + .consumed_mods = consumed_mods, + .composing = self.im_composing, + .utf8 = self.im_buf[0..self.im_len], + }) catch |err| { + log.err("error in key callback err={}", .{err}); + return 0; + }; + // If we consume the key then we want to reset the dead key state. + if (consumed) { + c.gtk_im_context_reset(self.im_context); + self.core_surface.preeditCallback(null) catch {}; return 1; } From 83ba2b9825ebacb36d224140a82bf38f250c8b23 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 16 Aug 2023 12:51:00 -0700 Subject: [PATCH 12/20] apprt/gtk: clean up keyval_unicode usage --- src/apprt/gtk.zig | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index 8d03c6720..d8b596ecc 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -1191,6 +1191,7 @@ pub const Surface = struct { ) callconv(.C) c.gboolean { const self = userdataSelf(ud.?); const mods = translateMods(gtk_mods); + const keyval_unicode = c.gdk_keyval_to_unicode(keyval); // We mark that we're in a keypress event. We use this in our // IM commit callback to determine if we need to send a char callback @@ -1233,7 +1234,6 @@ pub const Surface = struct { } // If that doesn't work then we try to translate they kevval.. - const keyval_unicode = c.gdk_keyval_to_unicode(keyval); if (keyval_unicode != 0) { if (std.math.cast(u8, keyval_unicode)) |byte| { if (input.Key.fromASCII(byte)) |key| { @@ -1279,14 +1279,11 @@ pub const Surface = struct { // a text value. We have to do this because GTK will not process // "Ctrl+Shift+1" (on US keyboards) as "Ctrl+!" but instead as "". // But the keyval is set correctly so we can at least extract that. - if (self.im_len == 0) { - const keyval_unicode = c.gdk_keyval_to_unicode(keyval); - if (keyval_unicode != 0) { - if (std.math.cast(u21, keyval_unicode)) |cp| { - if (std.unicode.utf8Encode(cp, &self.im_buf)) |len| { - self.im_len = len; - } else |_| {} - } + if (self.im_len == 0 and keyval_unicode > 0) { + if (std.math.cast(u21, keyval_unicode)) |cp| { + if (std.unicode.utf8Encode(cp, &self.im_buf)) |len| { + self.im_len = len; + } else |_| {} } } From 896d0e8fcf36248a961d9d71b1787a1825abc7ce Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 16 Aug 2023 13:02:31 -0700 Subject: [PATCH 13/20] apprt/gtk: only use key2callback --- src/apprt/gtk.zig | 169 +++++++++++++++++++++++----------------------- 1 file changed, 85 insertions(+), 84 deletions(-) diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index d8b596ecc..c3585f310 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -1155,33 +1155,6 @@ pub const Surface = struct { }; } - /// Key press event. This is where we do ALL of our key handling, - /// translation to keyboard layouts, dead key handling, etc. Key handling - /// is complicated so this comment will explain what's going on. - /// - /// At a high level, we want to do the following: - /// - /// 1. Emit a keyCallback for the key press with the right keys. - /// 2. Emit a charCallback if a unicode char was generated from the - /// keypresses, but only if keyCallback didn't consume the input. - /// - /// This callback will first set the "in_keypress" flag to true. This - /// lets our IM callbacks know that we're in a keypress event so they don't - /// emit a charCallback since this function will do it after the keyCallback - /// (remember, the order matters!). - /// - /// Next, we run the keypress through the input method context in order - /// to determine if we're in a dead key state, completed unicode char, etc. - /// This all happens through various callbacks: preedit, commit, etc. - /// These inspect "in_keypress" if they have to and set some instance - /// state. - /// - /// Finally, we map our keys to input.Keys, emit the keyCallback, then - /// emit the charCallback if we have to. - /// - /// Note we ALSO have an IMContext attached directly to the widget - /// which can emit preedit and commit callbacks. But, if we're not - /// in a keypress, we let those automatically work. fn gtkKeyPressed( ec_key: *c.GtkEventControllerKey, keyval: c.guint, @@ -1189,31 +1162,100 @@ pub const Surface = struct { gtk_mods: c.GdkModifierType, ud: ?*anyopaque, ) callconv(.C) c.gboolean { + return if (keyEvent(.press, ec_key, keyval, keycode, gtk_mods, ud)) 1 else 0; + } + + fn gtkKeyReleased( + ec_key: *c.GtkEventControllerKey, + keyval: c.guint, + keycode: c.guint, + state: c.GdkModifierType, + ud: ?*anyopaque, + ) callconv(.C) c.gboolean { + return if (keyEvent(.release, ec_key, keyval, keycode, state, ud)) 1 else 0; + } + + /// Key press event. This is where we do ALL of our key handling, + /// translation to keyboard layouts, dead key handling, etc. Key handling + /// is complicated so this comment will explain what's going on. + /// + /// At a high level, we want to construct an `input.KeyEvent` and + /// pass that to `keyCallback`. At a low level, this is more complicated + /// than it appears because we need to construct all of this information + /// and its not given to us. + /// + /// For press events, we run the keypress through the input method context + /// in order to determine if we're in a dead key state, completed unicode + /// char, etc. This all happens through various callbacks: preedit, commit, + /// etc. These inspect "in_keypress" if they have to and set some instance + /// state. + /// + /// We then take all of the information in order to determine if we have + /// a unicode character or if we have to map the keyval to a code to + /// get the underlying logical key, etc. + /// + /// Finally, we can emit the keyCallback. + /// + /// Note we ALSO have an IMContext attached directly to the widget + /// which can emit preedit and commit callbacks. But, if we're not + /// in a keypress, we let those automatically work. + fn keyEvent( + action: input.Action, + ec_key: *c.GtkEventControllerKey, + keyval: c.guint, + keycode: c.guint, + gtk_mods: c.GdkModifierType, + ud: ?*anyopaque, + ) bool { const self = userdataSelf(ud.?); const mods = translateMods(gtk_mods); const keyval_unicode = c.gdk_keyval_to_unicode(keyval); - - // We mark that we're in a keypress event. We use this in our - // IM commit callback to determine if we need to send a char callback - // to the core surface or not. - self.in_keypress = true; - defer self.in_keypress = false; + const event = c.gtk_event_controller_get_current_event(@ptrCast(ec_key)); // We always reset our committed text when ending a keypress so that // future keypresses don't think we have a commit event. defer self.im_len = 0; + // We only want to send the event through the IM context if we're a press + if (action == .press or action == .repeat) { + // We mark that we're in a keypress event. We use this in our + // IM commit callback to determine if we need to send a char callback + // to the core surface or not. + self.in_keypress = true; + defer self.in_keypress = false; + + // Pass the event through the IM controller to handle dead key states. + // Filter is true if the event was handled by the IM controller. + _ = c.gtk_im_context_filter_keypress(self.im_context, event) != 0; + + // If this is a dead key, then we're composing a character and + // we need to set our proper preedit state. + if (self.im_composing) preedit: { + const text = self.im_buf[0..self.im_len]; + const view = std.unicode.Utf8View.init(text) catch |err| { + log.warn("cannot build utf8 view over input: {}", .{err}); + break :preedit; + }; + var it = view.iterator(); + + const cp: u21 = it.nextCodepoint() orelse 0; + self.core_surface.preeditCallback(cp) catch |err| { + log.err("error in preedit callback err={}", .{err}); + break :preedit; + }; + } else { + // If we aren't composing, then we set our preedit to + // empty no matter what. + self.core_surface.preeditCallback(null) catch {}; + } + } + // We want to get the physical unmapped key to process physical keybinds. // (These are keybinds explicitly marked as requesting physical mapping). const physical_key = keycode: for (input.keycodes.entries) |entry| { if (entry.native == keycode) break :keycode entry.key; } else .invalid; - // Pass the event through the IM controller to handle dead key states. - // Filter is true if the event was handled by the IM controller. - const event = c.gtk_event_controller_get_current_event(@ptrCast(ec_key)); - _ = c.gtk_im_context_filter_keypress(self.im_context, event) != 0; - // Get our consumed modifiers const consumed_mods: input.Mods = consumed: { const raw = c.gdk_key_event_get_consumed_modifiers(event); @@ -1254,27 +1296,6 @@ pub const Surface = struct { // mods, // }); - // If this is a dead key, then we're composing a character and - // we need to set our proper preedit state. - if (self.im_composing) preedit: { - const text = self.im_buf[0..self.im_len]; - const view = std.unicode.Utf8View.init(text) catch |err| { - log.warn("cannot build utf8 view over input: {}", .{err}); - break :preedit; - }; - var it = view.iterator(); - - const cp: u21 = it.nextCodepoint() orelse 0; - self.core_surface.preeditCallback(cp) catch |err| { - log.err("error in preedit callback err={}", .{err}); - break :preedit; - }; - } else { - // If we aren't composing, then we set our preedit to - // empty no matter what. - self.core_surface.preeditCallback(null) catch {}; - } - // If we have no UTF-8 text, we try to convert our keyval to // a text value. We have to do this because GTK will not process // "Ctrl+Shift+1" (on US keyboards) as "Ctrl+!" but instead as "". @@ -1289,7 +1310,7 @@ pub const Surface = struct { // Invoke the core Ghostty logic to handle this input. const consumed = self.core_surface.key2Callback(.{ - .action = .press, + .action = action, .key = key, .physical_key = physical_key, .mods = mods, @@ -1298,37 +1319,17 @@ pub const Surface = struct { .utf8 = self.im_buf[0..self.im_len], }) catch |err| { log.err("error in key callback err={}", .{err}); - return 0; + return false; }; // If we consume the key then we want to reset the dead key state. - if (consumed) { + if (consumed and (action == .press or action == .repeat)) { c.gtk_im_context_reset(self.im_context); self.core_surface.preeditCallback(null) catch {}; - return 1; + return true; } - return 0; - } - - fn gtkKeyReleased( - _: *c.GtkEventControllerKey, - keyval: c.guint, - keycode: c.guint, - state: c.GdkModifierType, - ud: ?*anyopaque, - ) callconv(.C) c.gboolean { - _ = keycode; - - const key = translateKey(keyval); - const mods = translateMods(state); - const self = userdataSelf(ud.?); - const consumed = self.core_surface.keyCallback(.release, key, key, mods) catch |err| { - log.err("error in key callback err={}", .{err}); - return 0; - }; - - return if (consumed) 1 else 0; + return false; } fn gtkInputPreeditStart( From cd90b2a716230e404d27e4bb21720238f1581221 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 16 Aug 2023 13:26:22 -0700 Subject: [PATCH 14/20] apprt/embedded: only call new key2callback --- src/apprt/embedded.zig | 114 ++++++++++++++++++++--------------------- 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 83432a9dd..14182ea2a 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -387,8 +387,8 @@ pub const Surface = struct { keycode: u32, mods: input.Mods, ) !void { - // We don't handle release events because we don't use them yet. - if (action != .press and action != .repeat) return; + // True if this is a key down event + const is_down = action == .press or action == .repeat; // If we're on macOS and we have macos-option-as-alt enabled, // then we strip the alt modifier from the mods for translation. @@ -420,18 +420,43 @@ pub const Surface = struct { }; // Translate our key using the keymap for our localized keyboard layout. + // We only translate for keydown events. Otherwise, we only care about + // the raw keycode. var buf: [128]u8 = undefined; - const result = try self.app.keymap.translate( - &buf, - &self.keymap_state, - @intCast(keycode), - translate_mods, - ); + const result: input.Keymap.Translation = if (is_down) translate: { + const result = try self.app.keymap.translate( + &buf, + &self.keymap_state, + @intCast(keycode), + translate_mods, + ); - // If we aren't composing, then we set our preedit to empty no matter what. - if (!result.composing) { - self.core_surface.preeditCallback(null) catch {}; - } + // If this is a dead key, then we're composing a character and + // we need to set our proper preedit state. + if (result.composing) { + const view = std.unicode.Utf8View.init(result.text) catch |err| { + log.warn("cannot build utf8 view over input: {}", .{err}); + return; + }; + var it = view.iterator(); + + const cp: u21 = it.nextCodepoint() orelse 0; + self.core_surface.preeditCallback(cp) catch |err| { + log.err("error in preedit callback err={}", .{err}); + return; + }; + } else { + // If we aren't composing, then we set our preedit to + // empty no matter what. + self.core_surface.preeditCallback(null) catch {}; + } + + break :translate result; + } else .{ .composing = false, .text = "" }; + + // 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 .{}; // log.warn("TRANSLATE: action={} keycode={x} dead={} key_len={} key={any} key_str={s} mods={}", .{ // action, @@ -454,12 +479,14 @@ pub const Surface = struct { // charCallback. // // We also only do key translation if this is not a dead key. - const key = if (!result.composing and result.text.len == 1) key: { + const key = if (!result.composing) key: { // A completed key. If the length of the key is one then we can // attempt to translate it to a key enum and call the key // callback. First try plain ASCII. - if (input.Key.fromASCII(result.text[0])) |key| { - break :key key; + if (result.text.len > 0) { + if (input.Key.fromASCII(result.text[0])) |key| { + break :key key; + } } // If that doesn't work then we try to translate without @@ -481,53 +508,26 @@ pub const Surface = struct { break :key physical_key; } else .invalid; - // If both keys are invalid then we won't call the key callback. But - // if either one is valid, we want to give it a chance. - if (key != .invalid or physical_key != .invalid) { - const consumed = self.core_surface.keyCallback( - action, - key, - physical_key, - mods, - ) catch |err| { - log.err("error in key callback err={}", .{err}); - return; - }; - - // If we consume the key then we want to reset the dead key state. - if (consumed) { - self.keymap_state = .{}; - self.core_surface.preeditCallback(null) catch {}; - return; - } - } - - // No matter what happens next we'll want a utf8 view. - const view = std.unicode.Utf8View.init(result.text) catch |err| { - log.warn("cannot build utf8 view over input: {}", .{err}); + // Invoke the core Ghostty logic to handle this input. + const consumed = self.core_surface.key2Callback(.{ + .action = action, + .key = key, + .physical_key = physical_key, + .mods = mods, + .consumed_mods = consumed_mods, + .composing = result.composing, + .utf8 = result.text, + }) catch |err| { + log.err("error in key callback err={}", .{err}); return; }; - var it = view.iterator(); - - // If this is a dead key, then we're composing a character and - // we end processing here. We don't process keybinds for dead keys. - if (result.composing) { - const cp: u21 = it.nextCodepoint() orelse 0; - self.core_surface.preeditCallback(cp) catch |err| { - log.err("error in preedit callback err={}", .{err}); - return; - }; + // If we consume the key then we want to reset the dead key state. + if (consumed and is_down) { + self.keymap_state = .{}; + self.core_surface.preeditCallback(null) catch {}; return; } - - // Next, we want to call the char callback with each codepoint. - while (it.nextCodepoint()) |cp| { - self.core_surface.charCallback(cp, mods) catch |err| { - log.err("error in char callback err={}", .{err}); - return; - }; - } } pub fn charCallback(self: *Surface, cp_: u32) void { From dd385cc633df6b6d78df508e59fa709c64b5b626 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 16 Aug 2023 13:34:31 -0700 Subject: [PATCH 15/20] apprt/glfw: convert to new key2callback --- src/apprt/glfw.zig | 56 ++++++++++++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index bb00efafe..37faa73d1 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -282,10 +282,11 @@ pub const Surface = struct { /// A core surface core_surface: CoreSurface, - /// This is set to true when keyCallback consumes the input, suppressing - /// the charCallback from being fired. - key_consumed: bool = false, - key_mods: input.Mods = .{}, + /// This is the key event that was processed in keyCallback. This is only + /// non-null if the event was NOT consumed in keyCallback. This lets us + /// know in charCallback whether we should populate it and call it again. + /// (GLFW guarantees that charCallback is called after keyCallback). + key_event: ?input.KeyEvent = null, pub const Options = struct {}; @@ -592,14 +593,21 @@ pub const Surface = struct { const core_win = window.getUserPointer(CoreSurface) orelse return; - // If our keyCallback consumed the key input, don't emit a char. - if (core_win.rt_surface.key_consumed) { - core_win.rt_surface.key_consumed = false; - return; - } + // We need a key event in order to process the charcallback. If it + // isn't set then the key event was consumed. + var key_event = core_win.rt_surface.key_event orelse return; + core_win.rt_surface.key_event = null; - core_win.charCallback(codepoint, core_win.rt_surface.key_mods) catch |err| { - log.err("error in char callback err={}", .{err}); + // Populate the utf8 value for the event + var buf: [4]u8 = undefined; + const len = std.unicode.utf8Encode(codepoint, &buf) catch |err| { + log.err("error encoding codepoint={} err={}", .{ codepoint, err }); + return; + }; + key_event.utf8 = buf[0..len]; + + _ = core_win.key2Callback(key_event) catch |err| { + log.err("error in key callback err={}", .{err}); return; }; } @@ -755,18 +763,28 @@ pub const Surface = struct { => .invalid, }; - // TODO: we need to do mapped keybindings + const key_event: input.KeyEvent = .{ + .action = action, + .key = key, + .physical_key = key, + .mods = mods, + .consumed_mods = .{}, + .composing = false, + .utf8 = "", + }; - core_win.rt_surface.key_mods = mods; - core_win.rt_surface.key_consumed = core_win.keyCallback( - action, - key, - key, - mods, - ) catch |err| { + const consumed = core_win.key2Callback(key_event) catch |err| { log.err("error in key callback err={}", .{err}); return; }; + + // If it wasn't consumed, we set it on our self so that charcallback + // can make another attempt. Otherwise, we set null so the charcallback + // is ignored. + core_win.rt_surface.key_event = null; + if (!consumed and (action == .press or action == .repeat)) { + core_win.rt_surface.key_event = key_event; + } } fn focusCallback(window: glfw.Window, focused: bool) void { From 4e8f5d39970909af7a8f3b5852d7bafa1c48096f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 16 Aug 2023 13:39:44 -0700 Subject: [PATCH 16/20] remove charCallback/keyCallback --- src/Surface.zig | 377 ----------------------------------------- src/apprt/embedded.zig | 21 ++- src/apprt/gtk.zig | 19 ++- 3 files changed, 29 insertions(+), 388 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 779614641..077dc142e 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -987,158 +987,6 @@ pub fn preeditCallback(self: *Surface, preedit: ?u21) !void { try self.queueRender(); } -pub fn charCallback( - self: *Surface, - codepoint: u21, - mods: input.Mods, -) !void { - const tracy = trace(@src()); - defer tracy.end(); - - // Dev Mode - if (DevMode.enabled and DevMode.instance.visible) { - // If the event was handled by imgui, ignore it. - if (imgui.IO.get()) |io| { - if (io.cval().WantCaptureKeyboard) { - try self.queueRender(); - } - } else |_| {} - } - - // Critical area - const critical: struct { - alt_esc_prefix: bool, - modify_other_keys: bool, - } = critical: { - self.renderer_state.mutex.lock(); - defer self.renderer_state.mutex.unlock(); - - // Clear the selection if we have one. - if (self.io.terminal.screen.selection != null) { - self.setSelection(null); - try self.queueRender(); - } - - // We want to scroll to the bottom - // TODO: detect if we're at the bottom to avoid the render call here. - try self.io.terminal.scrollViewport(.{ .bottom = {} }); - - break :critical .{ - .alt_esc_prefix = self.io.terminal.modes.get(.alt_esc_prefix), - .modify_other_keys = self.io.terminal.flags.modify_other_keys_2, - }; - }; - - // Where we're going to write any data. Any data we write has to - // fit into the fixed size array so we just define it up front. - var data: termio.Message.WriteReq.Small.Array = undefined; - - // In modify other keys state 2, we send the CSI 27 sequence - // for any char with a modifier. Ctrl sequences like Ctrl+A - // are handled in keyCallback and should never have reached this - // point. - if (critical.modify_other_keys) { - // This copies xterm's `ModifyOtherKeys` function that returns - // whether modify other keys should be encoded for the given - // input. - const should_modify = should_modify: { - // xterm IsControlInput - if (codepoint >= 0x40 and codepoint <= 0x7F) - break :should_modify true; - - // If we have anything other than shift pressed, encode. - var mods_no_shift = mods; - mods_no_shift.shift = false; - if (!mods_no_shift.empty()) break :should_modify true; - - // We only have shift pressed. We only allow space. - if (codepoint == ' ') break :should_modify true; - - // This logic isn't complete but I don't fully understand - // the rest so I'm going to wait until we can have a - // reasonable test scenario. - break :should_modify false; - }; - - if (should_modify) { - for (input.function_keys.modifiers, 2..) |modset, code| { - if (!mods.equal(modset)) continue; - - const resp = try std.fmt.bufPrint( - &data, - "\x1B[27;{};{}~", - .{ code, codepoint }, - ); - _ = self.io_thread.mailbox.push(.{ - .write_small = .{ - .data = data, - .len = @intCast(resp.len), - }, - }, .{ .forever = {} }); - try self.io_thread.wakeup.notify(); - return; - } - } - } - - // Let's see if we should apply fixterms to this codepoint. - // At this stage of key processing, we only need to apply fixterms - // to unicode codepoints (the point of charCallback) if we have - // ctrl set. - if (mods.ctrl) { - const csi_u_mods = terminal.csi_u.Mods.fromInput(mods); - const resp = try std.fmt.bufPrint( - &data, - "\x1B[{};{}u", - .{ codepoint, csi_u_mods.seqInt() }, - ); - _ = self.io_thread.mailbox.push(.{ - .write_small = .{ - .data = data, - .len = @intCast(resp.len), - }, - }, .{ .forever = {} }); - try self.io_thread.wakeup.notify(); - return; - } - - // Prefix our data with ESC if we have alt pressed. - var i: u8 = 0; - if (mods.alt) alt: { - // If the terminal explicitly disabled this feature using mode 1036, - // then we don't send the prefix. - if (!critical.alt_esc_prefix) { - log.debug("alt_esc_prefix disabled with mode, not sending esc prefix", .{}); - break :alt; - } - - // On macOS, we have to opt-in to using alt because option - // by default is a unicode character sequence. - if (comptime builtin.target.isDarwin()) { - switch (self.config.macos_option_as_alt) { - .false => break :alt, - .true => {}, - .left => if (mods.sides.alt != .left) break :alt, - .right => if (mods.sides.alt != .right) break :alt, - } - } - - data[i] = 0x1b; - i += 1; - } - - const len = try std.unicode.utf8Encode(codepoint, data[i..]); - _ = self.io_thread.mailbox.push(.{ - .write_small = .{ - .data = data, - .len = len + i, - }, - }, .{ .forever = {} }); - - // After sending all our messages we have to notify our IO thread - try self.io_thread.wakeup.notify(); -} - pub fn key2Callback( self: *Surface, event: input.KeyEvent, @@ -1214,231 +1062,6 @@ pub fn key2Callback( return true; } -/// Called for a single key event. -/// -/// This will return true if the key was handled/consumed. In that case, -/// the caller doesn't need to call a subsequent `charCallback` for the -/// same event. However, the caller can call `charCallback` if they want, -/// the surface will retain state to ensure the event is ignored. -pub fn keyCallback( - self: *Surface, - action: input.Action, - key: input.Key, - physical_key: input.Key, - mods: input.Mods, -) !bool { - const tracy = trace(@src()); - defer tracy.end(); - - // log.warn("KEY CALLBACK action={} key={} physical_key={} mods={}", .{ - // action, - // key, - // physical_key, - // mods, - // }); - - // Dev Mode - if (DevMode.enabled and DevMode.instance.visible) { - // If the event was handled by imgui, ignore it. - if (imgui.IO.get()) |io| { - if (io.cval().WantCaptureKeyboard) { - try self.queueRender(); - } - } else |_| {} - } - - // We only handle press events - if (action != .press and action != .repeat) return false; - - // Mods for bindings never include caps/num lock. - const binding_mods = mods.binding(); - - // Check if we're processing a binding first. If so, that negates - // any further key processing. - { - const binding_action_: ?input.Binding.Action = action: { - var trigger: input.Binding.Trigger = .{ - .mods = binding_mods, - .key = key, - }; - - const set = self.config.keybind.set; - if (set.get(trigger)) |v| break :action v; - - trigger.key = physical_key; - trigger.physical = true; - if (set.get(trigger)) |v| break :action v; - - break :action null; - }; - - if (binding_action_) |binding_action| { - //log.warn("BINDING ACTION={}", .{binding_action}); - try self.performBindingAction(binding_action); - return true; - } - } - - // We'll need to know these values here on. - self.renderer_state.mutex.lock(); - const cursor_key_application = self.io.terminal.modes.get(.cursor_keys); - const keypad_key_application = self.io.terminal.modes.get(.keypad_keys); - const modify_other_keys = self.io.terminal.flags.modify_other_keys_2; - self.renderer_state.mutex.unlock(); - - // Check if we're processing a function key. - for (input.function_keys.keys.get(key)) |entry| { - switch (entry.cursor) { - .any => {}, - .normal => if (cursor_key_application) continue, - .application => if (!cursor_key_application) continue, - } - - switch (entry.keypad) { - .any => {}, - .normal => if (keypad_key_application) continue, - .application => if (!keypad_key_application) continue, - } - - switch (entry.modify_other_keys) { - .any => {}, - .set => if (modify_other_keys) continue, - .set_other => if (!modify_other_keys) continue, - } - - const mods_int = binding_mods.int(); - const entry_mods_int = entry.mods.int(); - if (entry_mods_int == 0) { - if (mods_int != 0 and !entry.mods_empty_is_any) continue; - // mods are either empty, or empty means any so we allow it. - } else if (entry_mods_int != mods_int) { - // any set mods require an exact match - continue; - } - - // log.debug("function key match: {}", .{entry}); - - // We found a match, send the sequence and return we as handled. - var data: termio.Message.WriteReq.Small.Array = undefined; - @memcpy(data[0..entry.sequence.len], entry.sequence); - _ = self.io_thread.mailbox.push(.{ - .write_small = .{ - .data = data, - .len = @intCast(entry.sequence.len), - }, - }, .{ .forever = {} }); - try self.io_thread.wakeup.notify(); - - return true; - } - - // If we have alt pressed, we're going to prefix any of the - // translations below with ESC (0x1B). - const alt = binding_mods.alt; - const unalt_mods = unalt_mods: { - var unalt_mods = binding_mods; - unalt_mods.alt = false; - break :unalt_mods unalt_mods; - }; - - // Handle non-printables - const char: u8 = char: { - const mods_int = unalt_mods.int(); - const ctrl_only = (input.Mods{ .ctrl = true }).int(); - - // If we're only pressing control, check if this is a character - // we convert to a non-printable. The best table I've found for - // this is: - // https://sw.kovidgoyal.net/kitty/keyboard-protocol/#legacy-ctrl-mapping-of-ascii-keys - // - // Note that depending on the apprt, these might be handled as - // composed characters. But not all app runtimes will do this; - // some only compose printable characters. So we manually handle - // this here. - if (mods_int != ctrl_only) break :char 0; - break :char switch (key) { - .space => 0, - .slash => 0x1F, - .zero => 0x30, - .one => 0x31, - .two => 0x00, - .three => 0x1B, - .four => 0x1C, - .five => 0x1D, - .six => 0x1E, - .seven => 0x1F, - .eight => 0x7F, - .nine => 0x39, - .backslash => 0x1C, - .left_bracket => 0x1B, - .right_bracket => 0x1D, - .a => 0x01, - .b => 0x02, - .c => 0x03, - .d => 0x04, - .e => 0x05, - .f => 0x06, - .g => 0x07, - .h => 0x08, - .i => 0x09, - .j => 0x0A, - .k => 0x0B, - .l => 0x0C, - .m => 0x0D, - .n => 0x0E, - .o => 0x0F, - .p => 0x10, - .q => 0x11, - .r => 0x12, - .s => 0x13, - .t => 0x14, - .u => 0x15, - .v => 0x16, - .w => 0x17, - .x => 0x18, - .y => 0x19, - .z => 0x1A, - else => 0, - }; - }; - if (char > 0) { - // Ask our IO thread to write the data - var data: termio.Message.WriteReq.Small.Array = undefined; - - // Write our data. If we need to alt-prefix we add that first. - var i: u8 = 0; - if (alt) { - data[i] = 0x1B; - i += 1; - } - data[i] = @intCast(char); - i += 1; - - _ = self.io_thread.mailbox.push(.{ - .write_small = .{ - .data = data, - .len = i, - }, - }, .{ .forever = {} }); - - // After sending all our messages we have to notify our IO thread - try self.io_thread.wakeup.notify(); - - // Control charactesr trigger a scroll - { - self.renderer_state.mutex.lock(); - defer self.renderer_state.mutex.unlock(); - self.scrollToBottom() catch |err| { - log.warn("error scrolling to bottom err={}", .{err}); - }; - } - - return true; - } - - return false; -} - pub fn focusCallback(self: *Surface, focused: bool) !void { // Notify our render thread of the new state _ = self.renderer_thread.mailbox.push(.{ diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 14182ea2a..04a868d07 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -532,8 +532,25 @@ pub const Surface = struct { pub fn charCallback(self: *Surface, cp_: u32) void { const cp = std.math.cast(u21, cp_) orelse return; - self.core_surface.charCallback(cp, .{}) catch |err| { - log.err("error in char callback err={}", .{err}); + var buf: [4]u8 = undefined; + const len = std.unicode.utf8Encode(cp, &buf) catch |err| { + log.err("error encoding codepoint={} err={}", .{ cp, err }); + return; + }; + + // For a char callback we just construct a key event with invalid + // keys but with text. This should result in the text being sent + // as-is. + _ = self.core_surface.key2Callback(.{ + .action = .press, + .key = .invalid, + .physical_key = .invalid, + .mods = .{}, + .consumed_mods = .{}, + .composing = false, + .utf8 = buf[0..len], + }) catch |err| { + log.err("error in key callback err={}", .{err}); return; }; } diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index c3585f310..0b7bca44e 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -1402,17 +1402,18 @@ pub const Surface = struct { // We're not in a keypress, so this was sent from an on-screen emoji // keyboard or someting like that. Send the characters directly to // the surface. - const view = std.unicode.Utf8View.init(str) catch |err| { - log.warn("cannot build utf8 view over input: {}", .{err}); + _ = self.core_surface.key2Callback(.{ + .action = .press, + .key = .invalid, + .physical_key = .invalid, + .mods = .{}, + .consumed_mods = .{}, + .composing = false, + .utf8 = str, + }) catch |err| { + log.err("error in key callback err={}", .{err}); return; }; - var it = view.iterator(); - while (it.nextCodepoint()) |cp| { - self.core_surface.charCallback(cp, .{}) catch |err| { - log.err("error in char callback err={}", .{err}); - return; - }; - } } fn gtkFocusEnter(_: *c.GtkEventControllerFocus, ud: ?*anyopaque) callconv(.C) void { From 33bef288506da3459d91d0bcda2ff431ca432def Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 16 Aug 2023 13:40:57 -0700 Subject: [PATCH 17/20] rename key2callback to keycallback --- src/Surface.zig | 5 ++++- src/apprt/embedded.zig | 4 ++-- src/apprt/glfw.zig | 4 ++-- src/apprt/gtk.zig | 4 ++-- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 077dc142e..d2a403c8b 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -987,7 +987,10 @@ pub fn preeditCallback(self: *Surface, preedit: ?u21) !void { try self.queueRender(); } -pub fn key2Callback( +/// Called for any key events. This handles keybindings, encoding and +/// sending to the termianl, etc. The return value is true if the key +/// was handled and false if it was not. +pub fn keyCallback( self: *Surface, event: input.KeyEvent, ) !bool { diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 04a868d07..68a3c805a 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -509,7 +509,7 @@ pub const Surface = struct { } else .invalid; // Invoke the core Ghostty logic to handle this input. - const consumed = self.core_surface.key2Callback(.{ + const consumed = self.core_surface.keyCallback(.{ .action = action, .key = key, .physical_key = physical_key, @@ -541,7 +541,7 @@ pub const Surface = struct { // For a char callback we just construct a key event with invalid // keys but with text. This should result in the text being sent // as-is. - _ = self.core_surface.key2Callback(.{ + _ = self.core_surface.keyCallback(.{ .action = .press, .key = .invalid, .physical_key = .invalid, diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 37faa73d1..6e823643d 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -606,7 +606,7 @@ pub const Surface = struct { }; key_event.utf8 = buf[0..len]; - _ = core_win.key2Callback(key_event) catch |err| { + _ = core_win.keyCallback(key_event) catch |err| { log.err("error in key callback err={}", .{err}); return; }; @@ -773,7 +773,7 @@ pub const Surface = struct { .utf8 = "", }; - const consumed = core_win.key2Callback(key_event) catch |err| { + const consumed = core_win.keyCallback(key_event) catch |err| { log.err("error in key callback err={}", .{err}); return; }; diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index 0b7bca44e..1a8d497db 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -1309,7 +1309,7 @@ pub const Surface = struct { } // Invoke the core Ghostty logic to handle this input. - const consumed = self.core_surface.key2Callback(.{ + const consumed = self.core_surface.keyCallback(.{ .action = action, .key = key, .physical_key = physical_key, @@ -1402,7 +1402,7 @@ pub const Surface = struct { // We're not in a keypress, so this was sent from an on-screen emoji // keyboard or someting like that. Send the characters directly to // the surface. - _ = self.core_surface.key2Callback(.{ + _ = self.core_surface.keyCallback(.{ .action = .press, .key = .invalid, .physical_key = .invalid, From dcf9cdd8bfd9497ff2296cbc8fce843c18da633f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 16 Aug 2023 13:57:17 -0700 Subject: [PATCH 18/20] input: CSI u encoding for modified unicode chars --- src/input/KeyEncoder.zig | 101 ++++++++++++++++++++++++++++++--------- src/terminal/csi_u.zig | 64 ------------------------- src/terminal/main.zig | 1 - 3 files changed, 78 insertions(+), 88 deletions(-) delete mode 100644 src/terminal/csi_u.zig diff --git a/src/input/KeyEncoder.zig b/src/input/KeyEncoder.zig index 3ac8a5fc9..639cf9209 100644 --- a/src/input/KeyEncoder.zig +++ b/src/input/KeyEncoder.zig @@ -29,6 +29,7 @@ pub fn legacy( self: *const KeyEncoder, buf: []u8, ) ![]const u8 { + const all_mods = self.event.mods; const effective_mods = self.event.effectiveMods(); const binding_mods = effective_mods.binding(); @@ -48,8 +49,10 @@ pub fn legacy( self.modify_other_keys_state_2, )) |sequence| return copyToBuf(buf, sequence); - // If we match a control sequence, we output that directly. - if (ctrlSeq(self.event.key, binding_mods)) |char| { + // If we match a control sequence, we output that directly. For + // ctrlSeq we have to use all mods because we want it to only + // match ctrl+. + if (ctrlSeq(self.event.key, all_mods)) |char| { // C0 sequences support alt-as-esc prefixing. if (binding_mods.alt) { if (buf.len < 2) return error.OutOfMemory; @@ -113,26 +116,22 @@ pub fn legacy( } } - // // Let's see if we should apply fixterms to this codepoint. - // // At this stage of key processing, we only need to apply fixterms - // // to unicode codepoints (the point of charCallback) if we have - // // ctrl set. - // if (mods.ctrl) { - // const csi_u_mods = terminal.csi_u.Mods.fromInput(mods); - // const resp = try std.fmt.bufPrint( - // &data, - // "\x1B[{};{}u", - // .{ codepoint, csi_u_mods.seqInt() }, - // ); - // _ = self.io_thread.mailbox.push(.{ - // .write_small = .{ - // .data = data, - // .len = @intCast(resp.len), - // }, - // }, .{ .forever = {} }); - // try self.io_thread.wakeup.notify(); - // return; - // } + // Let's see if we should apply fixterms to this codepoint. + // At this stage of key processing, we only need to apply fixterms + // to unicode codepoints if we have ctrl set. + if (self.event.mods.ctrl) { + // Important: we want to use the original + const csi_u_mods = CsiUMods.fromInput(binding_mods); + const result = try std.fmt.bufPrint( + buf, + "\x1B[{};{}u", + .{ utf8[0], csi_u_mods.seqInt() }, + ); + + std.log.warn("CSI_U: {s}", .{result}); + + return result; + } // If we have alt-pressed and alt-esc-prefix is enabled, then // we need to prefix the utf8 sequence with an esc. @@ -223,7 +222,7 @@ fn ctrlSeq(keyval: key.Key, mods: key.Mods) ?u8 { const unalt_mods = unalt_mods: { var unalt_mods = mods; unalt_mods.alt = false; - break :unalt_mods unalt_mods; + break :unalt_mods unalt_mods.binding(); }; // If we have any other modifier key set, then we do not generate @@ -281,6 +280,62 @@ fn ctrlSeq(keyval: key.Key, mods: key.Mods) ?u8 { }; } +/// This is the bitmask for fixterm CSI u modifiers. +const CsiUMods = packed struct(u3) { + shift: bool = false, + alt: bool = false, + ctrl: bool = false, + + /// Convert an input mods value into the CSI u mods value. + pub fn fromInput(mods: key.Mods) CsiUMods { + return .{ + .shift = mods.shift, + .alt = mods.alt, + .ctrl = mods.ctrl, + }; + } + + /// Returns the raw int value of this packed struct. + pub fn int(self: CsiUMods) u3 { + return @bitCast(self); + } + + /// Returns the integer value sent as part of the CSI u sequence. + /// This adds 1 to the bitmask value as described in the spec. + pub fn seqInt(self: CsiUMods) u4 { + const raw: u4 = @intCast(self.int()); + return raw + 1; + } +}; + +test "modifer sequence values" { + // This is all sort of trivially seen by looking at the code but + // we want to make sure we never regress this. + var mods: CsiUMods = .{}; + try testing.expectEqual(@as(u4, 1), mods.seqInt()); + + mods = .{ .shift = true }; + try testing.expectEqual(@as(u4, 2), mods.seqInt()); + + mods = .{ .alt = true }; + try testing.expectEqual(@as(u4, 3), mods.seqInt()); + + mods = .{ .ctrl = true }; + try testing.expectEqual(@as(u4, 5), mods.seqInt()); + + mods = .{ .alt = true, .shift = true }; + try testing.expectEqual(@as(u4, 4), mods.seqInt()); + + mods = .{ .ctrl = true, .shift = true }; + try testing.expectEqual(@as(u4, 6), mods.seqInt()); + + mods = .{ .alt = true, .ctrl = true }; + try testing.expectEqual(@as(u4, 7), mods.seqInt()); + + mods = .{ .alt = true, .ctrl = true, .shift = true }; + try testing.expectEqual(@as(u4, 8), mods.seqInt()); +} + test "legacy: ctrl+alt+c" { var buf: [128]u8 = undefined; var enc: KeyEncoder = .{ diff --git a/src/terminal/csi_u.zig b/src/terminal/csi_u.zig deleted file mode 100644 index 93b1d1e7d..000000000 --- a/src/terminal/csi_u.zig +++ /dev/null @@ -1,64 +0,0 @@ -//! This file has information related to Paul Evans's "fixterms" -//! encoding, also sometimes referred to as "CSI u" encoding. -//! -//! https://www.leonerd.org.uk/hacks/fixterms/ - -const std = @import("std"); - -const input = @import("../input.zig"); - -pub const Mods = packed struct(u3) { - shift: bool = false, - alt: bool = false, - ctrl: bool = false, - - /// Convert an input mods value into the CSI u mods value. - pub fn fromInput(mods: input.Mods) Mods { - return .{ - .shift = mods.shift, - .alt = mods.alt, - .ctrl = mods.ctrl, - }; - } - - /// Returns the raw int value of this packed struct. - pub fn int(self: Mods) u3 { - return @bitCast(self); - } - - /// Returns the integer value sent as part of the CSI u sequence. - /// This adds 1 to the bitmask value as described in the spec. - pub fn seqInt(self: Mods) u4 { - const raw: u4 = @intCast(self.int()); - return raw + 1; - } -}; - -test "modifer sequence values" { - // This is all sort of trivially seen by looking at the code but - // we want to make sure we never regress this. - const testing = std.testing; - var mods: Mods = .{}; - try testing.expectEqual(@as(u4, 1), mods.seqInt()); - - mods = .{ .shift = true }; - try testing.expectEqual(@as(u4, 2), mods.seqInt()); - - mods = .{ .alt = true }; - try testing.expectEqual(@as(u4, 3), mods.seqInt()); - - mods = .{ .ctrl = true }; - try testing.expectEqual(@as(u4, 5), mods.seqInt()); - - mods = .{ .alt = true, .shift = true }; - try testing.expectEqual(@as(u4, 4), mods.seqInt()); - - mods = .{ .ctrl = true, .shift = true }; - try testing.expectEqual(@as(u4, 6), mods.seqInt()); - - mods = .{ .alt = true, .ctrl = true }; - try testing.expectEqual(@as(u4, 7), mods.seqInt()); - - mods = .{ .alt = true, .ctrl = true, .shift = true }; - try testing.expectEqual(@as(u4, 8), mods.seqInt()); -} diff --git a/src/terminal/main.zig b/src/terminal/main.zig index f08c97326..3de537656 100644 --- a/src/terminal/main.zig +++ b/src/terminal/main.zig @@ -7,7 +7,6 @@ const csi = @import("csi.zig"); const sgr = @import("sgr.zig"); pub const point = @import("point.zig"); pub const color = @import("color.zig"); -pub const csi_u = @import("csi_u.zig"); pub const modes = @import("modes.zig"); pub const parse_table = @import("parse_table.zig"); From 2ff2e018ba6aedeea8e18a1926631a0b51cb4b45 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 16 Aug 2023 14:04:38 -0700 Subject: [PATCH 19/20] input: clarify why we use all mods for unicode CSI u --- src/input/KeyEncoder.zig | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/input/KeyEncoder.zig b/src/input/KeyEncoder.zig index 639cf9209..9dfc5f8ae 100644 --- a/src/input/KeyEncoder.zig +++ b/src/input/KeyEncoder.zig @@ -120,16 +120,17 @@ pub fn legacy( // At this stage of key processing, we only need to apply fixterms // to unicode codepoints if we have ctrl set. if (self.event.mods.ctrl) { - // Important: we want to use the original - const csi_u_mods = CsiUMods.fromInput(binding_mods); + // Important: we want to use the original mods here, not the + // effective mods. The fixterms spec states the shifted chars + // should be sent uppercase but Kitty changes that behavior + // so we'll send all the mods. + const csi_u_mods = CsiUMods.fromInput(self.event.mods); const result = try std.fmt.bufPrint( buf, "\x1B[{};{}u", .{ utf8[0], csi_u_mods.seqInt() }, ); - - std.log.warn("CSI_U: {s}", .{result}); - + // std.log.warn("CSI_U: {s}", .{result}); return result; } From 9cef09c58d91ab3c2bccdb95f24c0d6f602673e8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 16 Aug 2023 14:12:10 -0700 Subject: [PATCH 20/20] input: do not send ctrl-sequences for ctrl-i,m,[ --- src/input/KeyEncoder.zig | 53 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/src/input/KeyEncoder.zig b/src/input/KeyEncoder.zig index 9dfc5f8ae..11deeb942 100644 --- a/src/input/KeyEncoder.zig +++ b/src/input/KeyEncoder.zig @@ -249,7 +249,6 @@ fn ctrlSeq(keyval: key.Key, mods: key.Mods) ?u8 { .eight => 0x7F, .nine => 0x39, .backslash => 0x1C, - .left_bracket => 0x1B, .right_bracket => 0x1D, .a => 0x01, .b => 0x02, @@ -259,11 +258,9 @@ fn ctrlSeq(keyval: key.Key, mods: key.Mods) ?u8 { .f => 0x06, .g => 0x07, .h => 0x08, - .i => 0x09, .j => 0x0A, .k => 0x0B, .l => 0x0C, - .m => 0x0D, .n => 0x0E, .o => 0x0F, .p => 0x10, @@ -277,6 +274,14 @@ fn ctrlSeq(keyval: key.Key, mods: key.Mods) ?u8 { .x => 0x18, .y => 0x19, .z => 0x1A, + + // These are purposely NOT handled here because of the fixterms + // specification: https://www.leonerd.org.uk/hacks/fixterms/ + // These are processed as CSI u. + // .i => 0x09, + // .m => 0x0D, + // .left_bracket => 0x1B, + else => null, }; } @@ -404,6 +409,48 @@ test "legacy: ctrl+shift+char with modify other state 2" { try testing.expectEqualStrings("\x1b[27;6;72~", actual); } +test "legacy: fixterm awkward letters" { + var buf: [128]u8 = undefined; + { + var enc: KeyEncoder = .{ .event = .{ + .key = .i, + .mods = .{ .ctrl = true }, + .utf8 = "i", + } }; + const actual = try enc.legacy(&buf); + try testing.expectEqualStrings("\x1b[105;5u", actual); + } + { + var enc: KeyEncoder = .{ .event = .{ + .key = .m, + .mods = .{ .ctrl = true }, + .utf8 = "m", + } }; + const actual = try enc.legacy(&buf); + try testing.expectEqualStrings("\x1b[109;5u", actual); + } + { + var enc: KeyEncoder = .{ .event = .{ + .key = .left_bracket, + .mods = .{ .ctrl = true }, + .utf8 = "[", + } }; + const actual = try enc.legacy(&buf); + try testing.expectEqualStrings("\x1b[91;5u", actual); + } + { + // This doesn't exactly match the fixterm spec but matches the + // behavior of Kitty. + var enc: KeyEncoder = .{ .event = .{ + .key = .two, + .mods = .{ .ctrl = true, .shift = true }, + .utf8 = "@", + } }; + const actual = try enc.legacy(&buf); + try testing.expectEqualStrings("\x1b[64;6u", actual); + } +} + test "ctrlseq: normal ctrl c" { const seq = ctrlSeq(.c, .{ .ctrl = true }); try testing.expectEqual(@as(u8, 0x03), seq.?);