core: rework binding handling to prepare for nested binding sets

This commit is contained in:
Mitchell Hashimoto
2024-08-18 20:58:45 -07:00
parent ec050407ee
commit 4201a580f3
2 changed files with 143 additions and 76 deletions

View File

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

View File

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