From 4201a580f32e098024fab16fda6bb7cf711730a8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 18 Aug 2024 20:58:45 -0700 Subject: [PATCH] core: rework binding handling to prepare for nested binding sets --- src/Surface.zig | 196 ++++++++++++++++++++++++++++------------------ src/input/key.zig | 23 ++++++ 2 files changed, 143 insertions(+), 76 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 3651ea606..58b792552 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -72,6 +72,9 @@ renderer_thr: std.Thread, /// Mouse state. mouse: Mouse, +/// Keyboard input state. +keyboard: 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 /// 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, }; +/// 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 /// Config struct usually to prevent sharing a single value. const DerivedConfig = struct { @@ -428,6 +447,7 @@ pub fn init( }, .renderer_thr = undefined, .mouse = .{}, + .keyboard = .{}, .io = undefined, .io_thread = io_thread, .io_thr = undefined, @@ -1322,82 +1342,13 @@ pub fn keyCallback( } }; - // Before encoding, we see if we have any keybindings for this - // key. Those always intercept before any encoding tasks. - binding: { - const binding_entry: input.Binding.Set.Entry, const binding_trigger: input.Binding.Trigger = action: { - const binding_mods = event.mods.binding(); - var trigger: input.Binding.Trigger = .{ - .mods = binding_mods, - .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; - } - } + // Handle keybindings first. We need to handle this on all events + // (press, repeat, release) because a press may perform a binding but + // a release should not encode if we consumed the press. + if (try self.maybeHandleBinding( + event, + if (insp_ev) |*ev| ev else null, + )) |v| return v; // If we allow KAM and KAM is enabled then we do nothing. if (self.config.vt_kam_allowed) { @@ -1581,6 +1532,99 @@ pub fn keyCallback( 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 /// protocol. This will treat the input text as if it was pasted /// from the clipboard so the same logic will be applied. Namely, diff --git a/src/input/key.zig b/src/input/key.zig index 7886d0b55..8fc7c6f20 100644 --- a/src/input/key.zig +++ b/src/input/key.zig @@ -54,6 +54,29 @@ pub const KeyEvent = struct { if (self.utf8.len == 0) return self.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.