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.
This commit is contained in:
Mitchell Hashimoto
2025-01-03 13:40:47 -08:00
parent 8f5f432ab6
commit 4d103ca16d
3 changed files with 75 additions and 12 deletions

View File

@ -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,

View File

@ -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();

View File

@ -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.