//! Keybinding helpers: declare named bindings with help text, then match //! incoming `Msg.Key` events against them. //! //! const k = leaves.key; //! const up = k.Binding{ //! .keys = &.{ k.special(.up), k.rune('k') }, //! .help = .{ .key = "↑/k", .desc = "move up" }, //! }; //! if (up.matches(msg_key)) { ... } const std = @import("std"); const matcha = @import("matcha"); const Key = matcha.Msg.Key; pub const Help = struct { key: []const u8 = "", desc: []const u8 = "", }; pub const Binding = struct { keys: []const Key = &.{}, help: Help = .{}, disabled: bool = false, pub fn enabled(self: Binding) bool { return !self.disabled and self.keys.len > 0; } pub fn matches(self: Binding, k: Key) bool { if (!self.enabled()) return false; for (self.keys) |bound| if (keyEql(bound, k)) return true; return false; } }; /// Returns true if `k` matches any of the supplied bindings. pub fn matchesAny(k: Key, bindings: []const Binding) bool { for (bindings) |b| if (b.matches(k)) return true; return false; } fn keyEql(a: Key, b: Key) bool { if (a.kind != b.kind) return false; if (a.kind == .rune and a.rune != b.rune) return false; return @as(u8, @bitCast(a.mods)) == @as(u8, @bitCast(b.mods)); } pub fn rune(r: u21) Key { return .{ .kind = .rune, .rune = r }; } pub fn ctrl(r: u21) Key { return .{ .kind = .rune, .rune = r, .mods = .{ .ctrl = true } }; } pub fn alt(r: u21) Key { return .{ .kind = .rune, .rune = r, .mods = .{ .alt = true } }; } pub fn shift(r: u21) Key { return .{ .kind = .rune, .rune = r, .mods = .{ .shift = true } }; } pub fn special(kind: Key.Kind) Key { return .{ .kind = kind }; } test "binding matches rune" { const b: Binding = .{ .keys = &.{ rune('k'), special(.up) } }; try std.testing.expect(b.matches(.{ .kind = .rune, .rune = 'k' })); try std.testing.expect(b.matches(.{ .kind = .up })); try std.testing.expect(!b.matches(.{ .kind = .rune, .rune = 'j' })); } test "disabled binding never matches" { const b: Binding = .{ .keys = &.{rune('k')}, .disabled = true }; try std.testing.expect(!b.matches(.{ .kind = .rune, .rune = 'k' })); try std.testing.expect(!b.enabled()); } test "mods are required to match" { const b: Binding = .{ .keys = &.{ctrl('c')} }; try std.testing.expect(b.matches(.{ .kind = .rune, .rune = 'c', .mods = .{ .ctrl = true } })); try std.testing.expect(!b.matches(.{ .kind = .rune, .rune = 'c' })); } test "matchesAny" { const up: Binding = .{ .keys = &.{special(.up)} }; const down: Binding = .{ .keys = &.{special(.down)} }; try std.testing.expect(matchesAny(.{ .kind = .up }, &.{ up, down })); try std.testing.expect(!matchesAny(.{ .kind = .left }, &.{ up, down })); }