diff --git a/src/Surface.zig b/src/Surface.zig index e002d374e..d2a403c8b 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -987,189 +987,29 @@ 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; - } - } - } - - // 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(); -} - -/// 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. +/// 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, - action: input.Action, - key: input.Key, - physical_key: input.Key, - mods: input.Mods, + event: input.KeyEvent, ) !bool { - const tracy = trace(@src()); - defer tracy.end(); + // log.debug("keyCallback event={}", .{event}); - // 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. - { + // 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.effectiveMods().binding(); const binding_action_: ?input.Binding.Action = action: { var trigger: input.Binding.Trigger = .{ .mods = binding_mods, - .key = key, + .key = event.key, }; const set = self.config.keybind.set; if (set.get(trigger)) |v| break :action v; - trigger.key = physical_key; + trigger.key = event.physical_key; trigger.physical = true; if (set.get(trigger)) |v| break :action v; @@ -1183,164 +1023,46 @@ pub fn keyCallback( } } - // 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, + // 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, }; }; - 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; + 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 = i, - }, - }, .{ .forever = {} }); + _ = self.io_thread.mailbox.push(.{ + .write_small = .{ + .data = data, + .len = @intCast(seq.len), + }, + }, .{ .forever = {} }); + try self.io_thread.wakeup.notify(); - // 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; + // 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 false; + return true; } pub fn focusCallback(self: *Surface, focused: bool) !void { diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 2612022f5..68a3c805a 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. @@ -405,22 +405,58 @@ 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; }; // 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, @@ -443,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 @@ -470,59 +508,49 @@ 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.keyCallback(.{ + .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 { 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.keyCallback(.{ + .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/glfw.zig b/src/apprt/glfw.zig index bb00efafe..6e823643d 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.keyCallback(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.keyCallback(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 { diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index 3a949adef..1a8d497db 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,34 +1162,106 @@ 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); - - // 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 keyval_unicode = c.gdk_keyval_to_unicode(keyval); + 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; - - // 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. @@ -1231,7 +1276,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| { @@ -1252,100 +1296,40 @@ 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) { - 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(); - - const cp: u21 = it.nextCodepoint() orelse 0; - self.core_surface.preeditCallback(cp) catch |err| { - log.err("error in preedit callback err={}", .{err}); - return 0; - }; - - return 0; - } - - // 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) { - 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 |_| {} } } - // 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; - }; - } - - return 1; - } - - 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| { + // Invoke the core Ghostty logic to handle this input. + const consumed = self.core_surface.keyCallback(.{ + .action = action, + .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; + return false; }; - return if (consumed) 1 else 0; + // If we consume the key then we want to reset the dead key state. + if (consumed and (action == .press or action == .repeat)) { + c.gtk_im_context_reset(self.im_context); + self.core_surface.preeditCallback(null) catch {}; + return true; + } + + return false; } fn gtkInputPreeditStart( @@ -1418,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.keyCallback(.{ + .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 { 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..11deeb942 --- /dev/null +++ b/src/input/KeyEncoder.zig @@ -0,0 +1,471 @@ +/// 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"); +const function_keys = @import("function_keys.zig"); + +event: key.KeyEvent, + +/// 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, + +/// 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. +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(); + + // Legacy encoding only does press/repeat + 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, + binding_mods, + self.cursor_key_application, + self.keypad_key_application, + self.modify_other_keys_state_2, + )) |sequence| return copyToBuf(buf, sequence); + + // 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; + buf[0] = 0x1B; + buf[1] = char; + return buf[0..2]; + } + + if (buf.len < 1) return error.OutOfMemory; + buf[0] = char; + return buf[0..1]; + } + + // 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 }, + ); + } + } + } + + // 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 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}); + return result; + } + + // 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 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 +/// "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. +/// 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(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 = mods; + unalt_mods.alt = false; + break :unalt_mods unalt_mods.binding(); + }; + + // 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 (keyval) { + .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, + .right_bracket => 0x1D, + .a => 0x01, + .b => 0x02, + .c => 0x03, + .d => 0x04, + .e => 0x05, + .f => 0x06, + .g => 0x07, + .h => 0x08, + .j => 0x0A, + .k => 0x0B, + .l => 0x0C, + .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, + + // 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, + }; +} + +/// 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 = .{ + .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 "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 "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.?); +} + +test "ctrlseq: alt should be allowed" { + const seq = ctrlSeq(.c, .{ .alt = true, .ctrl = true }); + try testing.expectEqual(@as(u8, 0x03), seq.?); +} + +test "ctrlseq: no ctrl does nothing" { + try testing.expect(ctrlSeq(.c, .{}) == null); +} + +test "ctrlseq: shift does not generate ctrl seq" { + try testing.expect(ctrlSeq(.c, .{ .shift = true }) == null); + try testing.expect(ctrlSeq(.c, .{ .shift = true, .ctrl = true }) == null); +} diff --git a/src/input/key.zig b/src/input/key.zig index 8ec485c9a..622d50556 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 KeyEvent = 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: KeyEvent) 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;