From 4d103ca16d657ef95748fe4a141e9934217a0559 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 3 Jan 2025 13:40:47 -0800 Subject: [PATCH] core: add keyEventIsBinding This API can be used to determine if the next key event, if given as-is, would result in a key binding being triggered. --- include/ghostty.h | 1 + src/Surface.zig | 27 ++++++++++++++++++- src/apprt/embedded.zig | 59 ++++++++++++++++++++++++++++++++++-------- 3 files changed, 75 insertions(+), 12 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index cbb77f00c..b88fd9888 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -714,6 +714,7 @@ void ghostty_surface_set_color_scheme(ghostty_surface_t, ghostty_input_mods_e ghostty_surface_key_translation_mods(ghostty_surface_t, ghostty_input_mods_e); void ghostty_surface_key(ghostty_surface_t, ghostty_input_key_s); +bool ghostty_surface_key_is_binding(ghostty_surface_t, ghostty_input_key_s); void ghostty_surface_text(ghostty_surface_t, const char*, uintptr_t); bool ghostty_surface_mouse_captured(ghostty_surface_t); bool ghostty_surface_mouse_button(ghostty_surface_t, diff --git a/src/Surface.zig b/src/Surface.zig index 389e7f7e4..214bdae7e 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1637,13 +1637,38 @@ pub fn preeditCallback(self: *Surface, preedit_: ?[]const u8) !void { try self.queueRender(); } +/// Returns true if the given key event would trigger a keybinding +/// if it were to be processed. This is useful for determining if +/// a key event should be sent to the terminal or not. +/// +/// Note that this function does not check if the binding itself +/// is performable, only if the key event would trigger a binding. +/// If a performable binding is found and the event is not performable, +/// then Ghosty will act as though the binding does not exist. +pub fn keyEventIsBinding( + self: *Surface, + event: input.KeyEvent, +) bool { + switch (event.action) { + .release => return false, + .press, .repeat => {}, + } + + // Our keybinding set is either our current nested set (for + // sequences) or the root set. + const set = self.keyboard.bindings orelse &self.config.keybind.set; + + // If we have a keybinding for this event then we return true. + return set.getEvent(event) != null; +} + /// Called for any key events. This handles keybindings, encoding and /// sending to the terminal, etc. pub fn keyCallback( self: *Surface, event: input.KeyEvent, ) !InputEffect { - // log.debug("text keyCallback event={}", .{event}); + log.debug("text keyCallback event={}", .{event}); // Crash metadata in case we crash in here crash.sentry.thread_state = self.crashThreadState(); diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 59f81e694..3de9e4281 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -147,12 +147,12 @@ pub const App = struct { self.core_app.focusEvent(focused); } - /// See CoreApp.keyEvent. - pub fn keyEvent( + /// Convert a C key event into a Zig key event. + fn coreKeyEvent( self: *App, target: KeyTarget, event: KeyEvent, - ) !bool { + ) !?input.KeyEvent { const action = event.action; const keycode = event.keycode; const mods = event.mods; @@ -243,7 +243,7 @@ pub const App = struct { result.text, ) catch |err| { log.err("error in preedit callback err={}", .{err}); - return false; + return null; }, } } else { @@ -251,7 +251,7 @@ pub const App = struct { .app => {}, .surface => |surface| surface.core_surface.preeditCallback(null) catch |err| { log.err("error in preedit callback err={}", .{err}); - return false; + return null; }, } @@ -335,7 +335,7 @@ pub const App = struct { } else .invalid; // Build our final key event - const input_event: input.KeyEvent = .{ + return .{ .action = action, .key = key, .physical_key = physical_key, @@ -345,24 +345,39 @@ pub const App = struct { .utf8 = result.text, .unshifted_codepoint = unshifted_codepoint, }; + } + + /// See CoreApp.keyEvent. + pub fn keyEvent( + self: *App, + target: KeyTarget, + event: KeyEvent, + ) !bool { + // Convert our C key event into a Zig one. + const input_event: input.KeyEvent = (try self.coreKeyEvent( + target, + event, + )) orelse return false; // Invoke the core Ghostty logic to handle this input. const effect: CoreSurface.InputEffect = switch (target) { .app => if (self.core_app.keyEvent( self, input_event, - )) - .consumed - else - .ignored, + )) .consumed else .ignored, - .surface => |surface| try surface.core_surface.keyCallback(input_event), + .surface => |surface| try surface.core_surface.keyCallback( + input_event, + ), }; return switch (effect) { .closed => true, .ignored => false, .consumed => consumed: { + const is_down = input_event.action == .press or + input_event.action == .repeat; + if (is_down) { // If we consume the key then we want to reset the dead // key state. @@ -1601,6 +1616,28 @@ pub const CAPI = struct { }; } + /// Returns true if the given key event would trigger a binding + /// if it were sent to the surface right now. The "right now" + /// is important because things like trigger sequences are only + /// valid until the next key event. + export fn ghostty_surface_key_is_binding( + surface: *Surface, + event: KeyEvent, + ) bool { + const core_event = surface.app.coreKeyEvent( + .{ .surface = surface }, + event.keyEvent(), + ) catch |err| { + log.warn("error processing key event err={}", .{err}); + return false; + } orelse { + log.warn("error processing key event", .{}); + return false; + }; + + return surface.core_surface.keyEventIsBinding(core_event); + } + /// Send raw text to the terminal. This is treated like a paste /// so this isn't useful for sending escape sequences. For that, /// individual key input should be used.