mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
core: rework binding handling to prepare for nested binding sets
This commit is contained in:
196
src/Surface.zig
196
src/Surface.zig
@ -72,6 +72,9 @@ renderer_thr: std.Thread,
|
|||||||
/// Mouse state.
|
/// Mouse state.
|
||||||
mouse: Mouse,
|
mouse: Mouse,
|
||||||
|
|
||||||
|
/// Keyboard input state.
|
||||||
|
keyboard: Keyboard,
|
||||||
|
|
||||||
/// A currently pressed key. This is used so that we can send a keyboard
|
/// A currently pressed key. This is used so that we can send a keyboard
|
||||||
/// release event when the surface is unfocused. Note that when the surface
|
/// release event when the surface is unfocused. Note that when the surface
|
||||||
/// is refocused, a key press event may not be sent again -- this depends
|
/// is refocused, a key press event may not be sent again -- this depends
|
||||||
@ -192,6 +195,22 @@ const Mouse = struct {
|
|||||||
link_point: ?terminal.point.Coordinate = null,
|
link_point: ?terminal.point.Coordinate = null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Keyboard state for the surface.
|
||||||
|
pub const Keyboard = struct {
|
||||||
|
/// The currently active keybindings for the surface. This is used to
|
||||||
|
/// implement sequences: as leader keys are pressed, the active bindings
|
||||||
|
/// set is updated to reflect the current leader key sequence. If this is
|
||||||
|
/// null then the root bindings are used.
|
||||||
|
bindings: ?*const input.Binding.Set = null,
|
||||||
|
|
||||||
|
/// The last handled binding. This is used to prevent encoding release
|
||||||
|
/// events for handled bindings. We only need to keep track of one because
|
||||||
|
/// at least at the time of writing this, its impossible for two keys of
|
||||||
|
/// a combination to be handled by different bindings before the release
|
||||||
|
/// of the prior (namely since you can't bind modifier-only).
|
||||||
|
last_trigger: ?u64 = null,
|
||||||
|
};
|
||||||
|
|
||||||
/// The configuration that a surface has, this is copied from the main
|
/// The configuration that a surface has, this is copied from the main
|
||||||
/// Config struct usually to prevent sharing a single value.
|
/// Config struct usually to prevent sharing a single value.
|
||||||
const DerivedConfig = struct {
|
const DerivedConfig = struct {
|
||||||
@ -428,6 +447,7 @@ pub fn init(
|
|||||||
},
|
},
|
||||||
.renderer_thr = undefined,
|
.renderer_thr = undefined,
|
||||||
.mouse = .{},
|
.mouse = .{},
|
||||||
|
.keyboard = .{},
|
||||||
.io = undefined,
|
.io = undefined,
|
||||||
.io_thread = io_thread,
|
.io_thread = io_thread,
|
||||||
.io_thr = undefined,
|
.io_thr = undefined,
|
||||||
@ -1322,82 +1342,13 @@ pub fn keyCallback(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Before encoding, we see if we have any keybindings for this
|
// Handle keybindings first. We need to handle this on all events
|
||||||
// key. Those always intercept before any encoding tasks.
|
// (press, repeat, release) because a press may perform a binding but
|
||||||
binding: {
|
// a release should not encode if we consumed the press.
|
||||||
const binding_entry: input.Binding.Set.Entry, const binding_trigger: input.Binding.Trigger = action: {
|
if (try self.maybeHandleBinding(
|
||||||
const binding_mods = event.mods.binding();
|
event,
|
||||||
var trigger: input.Binding.Trigger = .{
|
if (insp_ev) |*ev| ev else null,
|
||||||
.mods = binding_mods,
|
)) |v| return v;
|
||||||
.key = .{ .translated = event.key },
|
|
||||||
};
|
|
||||||
|
|
||||||
const set = self.config.keybind.set;
|
|
||||||
if (set.get(trigger)) |v| break :action .{
|
|
||||||
v,
|
|
||||||
trigger,
|
|
||||||
};
|
|
||||||
|
|
||||||
trigger.key = .{ .physical = event.physical_key };
|
|
||||||
if (set.get(trigger)) |v| break :action .{
|
|
||||||
v,
|
|
||||||
trigger,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (event.unshifted_codepoint > 0) {
|
|
||||||
trigger.key = .{ .unicode = event.unshifted_codepoint };
|
|
||||||
if (set.get(trigger)) |v| break :action .{
|
|
||||||
v,
|
|
||||||
trigger,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
break :binding;
|
|
||||||
};
|
|
||||||
const binding_action = switch (binding_entry) {
|
|
||||||
.leader => {
|
|
||||||
// TODO
|
|
||||||
log.warn("sequenced keybinds are not supported yet", .{});
|
|
||||||
break :binding;
|
|
||||||
},
|
|
||||||
.action, .action_unconsumed => |action| action,
|
|
||||||
};
|
|
||||||
const consumed = binding_entry == .action;
|
|
||||||
|
|
||||||
// We only execute the binding on press/repeat but we still consume
|
|
||||||
// the key on release so that we don't send any release events.
|
|
||||||
log.debug("key event binding consumed={} action={}", .{ consumed, binding_action });
|
|
||||||
const performed = if (event.action == .press or event.action == .repeat) press: {
|
|
||||||
self.last_binding_trigger = 0;
|
|
||||||
break :press try self.performBindingAction(binding_action);
|
|
||||||
} else false;
|
|
||||||
|
|
||||||
// If we performed an action and it was a closing action,
|
|
||||||
// our "self" pointer is not safe to use anymore so we need to
|
|
||||||
// just exit immediately.
|
|
||||||
if (performed and closingAction(binding_action)) {
|
|
||||||
log.debug("key binding is a closing binding, halting key event processing", .{});
|
|
||||||
return .closed;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we consume this event, then we are done. If we don't consume
|
|
||||||
// it, we processed the action but we still want to process our
|
|
||||||
// encodings, too.
|
|
||||||
if (consumed and performed) {
|
|
||||||
self.last_binding_trigger = binding_trigger.hash();
|
|
||||||
if (insp_ev) |*ev| ev.binding = binding_action;
|
|
||||||
return .consumed;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have a previous binding trigger and it matches this one,
|
|
||||||
// then we handled the down event so we don't want to send any further
|
|
||||||
// events.
|
|
||||||
if (self.last_binding_trigger > 0 and
|
|
||||||
self.last_binding_trigger == binding_trigger.hash())
|
|
||||||
{
|
|
||||||
return .consumed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we allow KAM and KAM is enabled then we do nothing.
|
// If we allow KAM and KAM is enabled then we do nothing.
|
||||||
if (self.config.vt_kam_allowed) {
|
if (self.config.vt_kam_allowed) {
|
||||||
@ -1581,6 +1532,99 @@ pub fn keyCallback(
|
|||||||
return .consumed;
|
return .consumed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Maybe handles a binding for a given event and if so returns the effect.
|
||||||
|
/// Returns null if the event is not handled in any way and processing should
|
||||||
|
/// continue.
|
||||||
|
fn maybeHandleBinding(
|
||||||
|
self: *Surface,
|
||||||
|
event: input.KeyEvent,
|
||||||
|
insp_ev: ?*inspector.key.Event,
|
||||||
|
) !?InputEffect {
|
||||||
|
switch (event.action) {
|
||||||
|
// Release events never trigger a binding but we need to check if
|
||||||
|
// we consumed the press event so we don't encode the release.
|
||||||
|
.release => if (self.keyboard.last_trigger) |last| {
|
||||||
|
if (last == event.bindingHash()) {
|
||||||
|
// We don't reset the last trigger on release because
|
||||||
|
// an apprt may send multiple release events for a single
|
||||||
|
// press event.
|
||||||
|
return .consumed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Carry on processing.
|
||||||
|
.press, .repeat => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find an entry in the keybind set that matches our event.
|
||||||
|
const entry: input.Binding.Set.Entry = entry: {
|
||||||
|
const set = self.keyboard.bindings orelse &self.config.keybind.set;
|
||||||
|
|
||||||
|
var trigger: input.Binding.Trigger = .{
|
||||||
|
.mods = event.mods.binding(),
|
||||||
|
.key = .{ .translated = event.key },
|
||||||
|
};
|
||||||
|
if (set.get(trigger)) |v| break :entry v;
|
||||||
|
|
||||||
|
trigger.key = .{ .physical = event.physical_key };
|
||||||
|
if (set.get(trigger)) |v| break :entry v;
|
||||||
|
|
||||||
|
if (event.unshifted_codepoint > 0) {
|
||||||
|
trigger.key = .{ .unicode = event.unshifted_codepoint };
|
||||||
|
if (set.get(trigger)) |v| break :entry v;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No entry found. If we're not looking at the root set of the
|
||||||
|
// bindings we need to encode everything up to this point and
|
||||||
|
// send to the pty.
|
||||||
|
if (self.keyboard.bindings != null) @panic("TODO");
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Determine if this entry has an action or if its a leader key.
|
||||||
|
const action: input.Binding.Action, const consumed: bool = switch (entry) {
|
||||||
|
.leader => {
|
||||||
|
// TODO
|
||||||
|
log.warn("sequenced keybinds are not supported yet", .{});
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
.action => |v| .{ v, true },
|
||||||
|
.action_unconsumed => |v| .{ v, false },
|
||||||
|
};
|
||||||
|
|
||||||
|
// We have an action, so at this point we're handling SOMETHING so
|
||||||
|
// we reset the last trigger to null. We only set this if we actually
|
||||||
|
// perform an action (below)
|
||||||
|
self.keyboard.last_trigger = null;
|
||||||
|
|
||||||
|
// Attempt to perform the action
|
||||||
|
log.debug("key event binding consumed={} action={}", .{ consumed, action });
|
||||||
|
const performed = try self.performBindingAction(action);
|
||||||
|
|
||||||
|
// If we performed an action and it was a closing action,
|
||||||
|
// our "self" pointer is not safe to use anymore so we need to
|
||||||
|
// just exit immediately.
|
||||||
|
if (performed and closingAction(action)) {
|
||||||
|
log.debug("key binding is a closing binding, halting key event processing", .{});
|
||||||
|
return .closed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we consume this event, then we are done. If we don't consume
|
||||||
|
// it, we processed the action but we still want to process our
|
||||||
|
// encodings, too.
|
||||||
|
if (performed and consumed) {
|
||||||
|
self.keyboard.last_trigger = event.bindingHash();
|
||||||
|
if (insp_ev) |ev| ev.binding = action;
|
||||||
|
return .consumed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// Sends text as-is to the terminal without triggering any keyboard
|
/// Sends text as-is to the terminal without triggering any keyboard
|
||||||
/// protocol. This will treat the input text as if it was pasted
|
/// protocol. This will treat the input text as if it was pasted
|
||||||
/// from the clipboard so the same logic will be applied. Namely,
|
/// from the clipboard so the same logic will be applied. Namely,
|
||||||
|
@ -54,6 +54,29 @@ pub const KeyEvent = struct {
|
|||||||
if (self.utf8.len == 0) return self.mods;
|
if (self.utf8.len == 0) return self.mods;
|
||||||
return self.mods.unset(self.consumed_mods);
|
return self.mods.unset(self.consumed_mods);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a unique hash for this key event to be used for tracking
|
||||||
|
/// uniquess specifically with bindings. This omits fields that are
|
||||||
|
/// irrelevant for bindings.
|
||||||
|
pub fn bindingHash(self: KeyEvent) u64 {
|
||||||
|
var hasher = std.hash.Wyhash.init(0);
|
||||||
|
|
||||||
|
// These are all the fields that are explicitly part of Trigger.
|
||||||
|
std.hash.autoHash(&hasher, self.key);
|
||||||
|
std.hash.autoHash(&hasher, self.physical_key);
|
||||||
|
std.hash.autoHash(&hasher, self.unshifted_codepoint);
|
||||||
|
std.hash.autoHash(&hasher, self.mods.binding());
|
||||||
|
|
||||||
|
// Notes on unmapped things and why:
|
||||||
|
//
|
||||||
|
// - action: we don't have action-specific bindings right now
|
||||||
|
// AND we want to know if a key resulted in a binding regardless
|
||||||
|
// of action because a press should also ignore a release and so on.
|
||||||
|
//
|
||||||
|
// We can add to this if there is other confusion.
|
||||||
|
|
||||||
|
return hasher.final();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A bitmask for all key modifiers.
|
/// A bitmask for all key modifiers.
|
||||||
|
Reference in New Issue
Block a user