Merge pull request #2418 from ghostty-org/apprt-key-seq

macOS: Key Sequence UI
This commit is contained in:
Mitchell Hashimoto
2024-10-09 09:30:53 -07:00
committed by GitHub
14 changed files with 241 additions and 71 deletions

View File

@ -500,6 +500,12 @@ typedef enum {
GHOSTTY_RENDERER_HEALTH_UNHEALTHY, GHOSTTY_RENDERER_HEALTH_UNHEALTHY,
} ghostty_action_renderer_health_e; } ghostty_action_renderer_health_e;
// apprt.action.KeySequence
typedef struct {
bool active;
ghostty_input_trigger_s trigger;
} ghostty_action_key_sequence_s;
// apprt.Action.Key // apprt.Action.Key
typedef enum { typedef enum {
GHOSTTY_ACTION_NEW_WINDOW, GHOSTTY_ACTION_NEW_WINDOW,
@ -531,6 +537,7 @@ typedef enum {
GHOSTTY_ACTION_OPEN_CONFIG, GHOSTTY_ACTION_OPEN_CONFIG,
GHOSTTY_ACTION_QUIT_TIMER, GHOSTTY_ACTION_QUIT_TIMER,
GHOSTTY_ACTION_SECURE_INPUT, GHOSTTY_ACTION_SECURE_INPUT,
GHOSTTY_ACTION_KEY_SEQUENCE,
} ghostty_action_tag_e; } ghostty_action_tag_e;
typedef union { typedef union {
@ -551,6 +558,7 @@ typedef union {
ghostty_action_renderer_health_e renderer_health; ghostty_action_renderer_health_e renderer_health;
ghostty_action_quit_timer_e quit_timer; ghostty_action_quit_timer_e quit_timer;
ghostty_action_secure_input_e secure_input; ghostty_action_secure_input_e secure_input;
ghostty_action_key_sequence_s key_sequence;
} ghostty_action_u; } ghostty_action_u;
typedef struct { typedef struct {

View File

@ -515,6 +515,9 @@ extension Ghostty {
case GHOSTTY_ACTION_TOGGLE_VISIBILITY: case GHOSTTY_ACTION_TOGGLE_VISIBILITY:
toggleVisibility(app, target: target) toggleVisibility(app, target: target)
case GHOSTTY_ACTION_KEY_SEQUENCE:
keySequence(app, target: target, v: action.action.key_sequence)
case GHOSTTY_ACTION_CLOSE_ALL_WINDOWS: case GHOSTTY_ACTION_CLOSE_ALL_WINDOWS:
fallthrough fallthrough
case GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW: case GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW:
@ -1071,6 +1074,38 @@ extension Ghostty {
} }
} }
private static func keySequence(
_ app: ghostty_app_t,
target: ghostty_target_s,
v: ghostty_action_key_sequence_s) {
switch (target.tag) {
case GHOSTTY_TARGET_APP:
Ghostty.logger.warning("key sequence does nothing with an app target")
return
case GHOSTTY_TARGET_SURFACE:
guard let surface = target.target.surface else { return }
guard let surfaceView = self.surfaceView(from: surface) else { return }
if v.active {
NotificationCenter.default.post(
name: Notification.didContinueKeySequence,
object: surfaceView,
userInfo: [
Notification.KeySequenceKey: keyEquivalent(for: v.trigger) as Any
]
)
} else {
NotificationCenter.default.post(
name: Notification.didEndKeySequence,
object: surfaceView
)
}
default:
assertionFailure()
}
}
// MARK: User Notifications // MARK: User Notifications
/// Handle a received user notification. This is called when a user notification is clicked or dismissed by the user /// Handle a received user notification. This is called when a user notification is clicked or dismissed by the user

View File

@ -87,25 +87,6 @@ extension Ghostty {
#if os(macOS) #if os(macOS)
// MARK: - Keybindings // MARK: - Keybindings
/// A convenience struct that has the key + modifiers for some keybinding.
struct KeyEquivalent: CustomStringConvertible {
let key: String
let modifiers: NSEvent.ModifierFlags
var description: String {
var key = self.key
// Note: the order below matters; it matches the ordering modifiers
// shown for macOS menu shortcut labels.
if modifiers.contains(.command) { key = "\(key)" }
if modifiers.contains(.shift) { key = "\(key)" }
if modifiers.contains(.option) { key = "\(key)" }
if modifiers.contains(.control) { key = "\(key)" }
return key
}
}
/// Return the key equivalent for the given action. The action is the name of the action /// Return the key equivalent for the given action. The action is the name of the action
/// in the Ghostty configuration. For example `keybind = cmd+q=quit` in Ghostty /// in the Ghostty configuration. For example `keybind = cmd+q=quit` in Ghostty
/// configuration would be "quit" action. /// configuration would be "quit" action.
@ -115,33 +96,7 @@ extension Ghostty {
guard let cfg = self.config else { return nil } guard let cfg = self.config else { return nil }
let trigger = ghostty_config_trigger(cfg, action, UInt(action.count)) let trigger = ghostty_config_trigger(cfg, action, UInt(action.count))
let equiv: String return Ghostty.keyEquivalent(for: trigger)
switch (trigger.tag) {
case GHOSTTY_TRIGGER_TRANSLATED:
if let v = Ghostty.keyEquivalent(key: trigger.key.translated) {
equiv = v
} else {
return nil
}
case GHOSTTY_TRIGGER_PHYSICAL:
if let v = Ghostty.keyEquivalent(key: trigger.key.physical) {
equiv = v
} else {
return nil
}
case GHOSTTY_TRIGGER_UNICODE:
equiv = String(trigger.key.unicode)
default:
return nil
}
return KeyEquivalent(
key: equiv,
modifiers: Ghostty.eventModifierFlags(mods: trigger.mods)
)
} }
#endif #endif

View File

@ -2,11 +2,68 @@ import Cocoa
import GhosttyKit import GhosttyKit
extension Ghostty { extension Ghostty {
// MARK: Key Equivalents
/// Returns the "keyEquivalent" string for a given input key. This doesn't always have a corresponding key. /// Returns the "keyEquivalent" string for a given input key. This doesn't always have a corresponding key.
static func keyEquivalent(key: ghostty_input_key_e) -> String? { static func keyEquivalent(key: ghostty_input_key_e) -> String? {
return Self.keyToEquivalent[key] return Self.keyToEquivalent[key]
} }
/// A convenience struct that has the key + modifiers for some keybinding.
struct KeyEquivalent: CustomStringConvertible {
let key: String
let modifiers: NSEvent.ModifierFlags
var description: String {
var key = self.key
// Note: the order below matters; it matches the ordering modifiers
// shown for macOS menu shortcut labels.
if modifiers.contains(.command) { key = "\(key)" }
if modifiers.contains(.shift) { key = "\(key)" }
if modifiers.contains(.option) { key = "\(key)" }
if modifiers.contains(.control) { key = "\(key)" }
return key
}
}
/// Return the key equivalent for the given trigger.
///
/// Returns nil if the trigger can't be processed. This should only happen for unknown trigger types
/// or keys.
static func keyEquivalent(for trigger: ghostty_input_trigger_s) -> KeyEquivalent? {
let equiv: String
switch (trigger.tag) {
case GHOSTTY_TRIGGER_TRANSLATED:
if let v = Ghostty.keyEquivalent(key: trigger.key.translated) {
equiv = v
} else {
return nil
}
case GHOSTTY_TRIGGER_PHYSICAL:
if let v = Ghostty.keyEquivalent(key: trigger.key.physical) {
equiv = v
} else {
return nil
}
case GHOSTTY_TRIGGER_UNICODE:
equiv = String(trigger.key.unicode)
default:
return nil
}
return KeyEquivalent(
key: equiv,
modifiers: Ghostty.eventModifierFlags(mods: trigger.mods)
)
}
// MARK: Mods
/// Returns the event modifier flags set for the Ghostty mods enum. /// Returns the event modifier flags set for the Ghostty mods enum.
static func eventModifierFlags(mods: ghostty_input_mods_e) -> NSEvent.ModifierFlags { static func eventModifierFlags(mods: ghostty_input_mods_e) -> NSEvent.ModifierFlags {
var flags = NSEvent.ModifierFlags(rawValue: 0); var flags = NSEvent.ModifierFlags(rawValue: 0);

View File

@ -262,7 +262,12 @@ extension Ghostty.Notification {
/// Notification that renderer health changed /// Notification that renderer health changed
static let didUpdateRendererHealth = Notification.Name("com.mitchellh.ghostty.didUpdateRendererHealth") static let didUpdateRendererHealth = Notification.Name("com.mitchellh.ghostty.didUpdateRendererHealth")
/// Notifications related to key sequences
static let didContinueKeySequence = Notification.Name("com.mitchellh.ghostty.didContinueKeySequence")
static let didEndKeySequence = Notification.Name("com.mitchellh.ghostty.didEndKeySequence")
static let KeySequenceKey = didContinueKeySequence.rawValue + ".key"
} }
// Make the input enum hashable. // Make the input enum hashable.
extension ghostty_input_key_e : Hashable {} extension ghostty_input_key_e : @retroactive Hashable {}

View File

@ -184,6 +184,34 @@ extension Ghostty {
} }
.ghosttySurfaceView(surfaceView) .ghosttySurfaceView(surfaceView)
#if canImport(AppKit)
// If we are in the middle of a key sequence, then we show a visual element. We only
// support this on macOS currently although in theory we can support mobile with keyboards!
if !surfaceView.keySequence.isEmpty {
let padding: CGFloat = 5
VStack {
Spacer()
HStack {
Text(verbatim: "Pending Key Sequence:")
ForEach(0..<surfaceView.keySequence.count, id: \.description) { index in
let key = surfaceView.keySequence[index]
Text(verbatim: key.description)
.font(.system(.body, design: .monospaced))
.padding(3)
.background(
RoundedRectangle(cornerRadius: 5)
.fill(Color(NSColor.selectedTextBackgroundColor))
)
}
}
.padding(.init(top: padding, leading: padding, bottom: padding, trailing: padding))
.frame(maxWidth: .infinity)
.background(.background)
}
}
#endif
// If we have a URL from hovering a link, we show that. // If we have a URL from hovering a link, we show that.
if let url = surfaceView.hoverUrl { if let url = surfaceView.hoverUrl {
let padding: CGFloat = 3 let padding: CGFloat = 3

View File

@ -30,6 +30,9 @@ extension Ghostty {
// The hovered URL string // The hovered URL string
@Published var hoverUrl: String? = nil @Published var hoverUrl: String? = nil
// The currently active key sequence. The sequence is not active if this is empty.
@Published var keySequence: [Ghostty.KeyEquivalent] = []
// The time this surface last became focused. This is a ContinuousClock.Instant // The time this surface last became focused. This is a ContinuousClock.Instant
// on supported platforms. // on supported platforms.
@Published var focusInstant: Any? = nil @Published var focusInstant: Any? = nil
@ -132,6 +135,16 @@ extension Ghostty {
selector: #selector(onUpdateRendererHealth), selector: #selector(onUpdateRendererHealth),
name: Ghostty.Notification.didUpdateRendererHealth, name: Ghostty.Notification.didUpdateRendererHealth,
object: self) object: self)
center.addObserver(
self,
selector: #selector(ghosttyDidContinueKeySequence),
name: Ghostty.Notification.didContinueKeySequence,
object: self)
center.addObserver(
self,
selector: #selector(ghosttyDidEndKeySequence),
name: Ghostty.Notification.didEndKeySequence,
object: self)
center.addObserver( center.addObserver(
self, self,
selector: #selector(windowDidChangeScreen), selector: #selector(windowDidChangeScreen),
@ -316,6 +329,16 @@ extension Ghostty {
healthy = health == GHOSTTY_RENDERER_HEALTH_OK healthy = health == GHOSTTY_RENDERER_HEALTH_OK
} }
@objc private func ghosttyDidContinueKeySequence(notification: SwiftUI.Notification) {
guard let keyAny = notification.userInfo?[Ghostty.Notification.KeySequenceKey] else { return }
guard let key = keyAny as? Ghostty.KeyEquivalent else { return }
keySequence.append(key)
}
@objc private func ghosttyDidEndKeySequence(notification: SwiftUI.Notification) {
keySequence = []
}
@objc private func windowDidChangeScreen(notification: SwiftUI.Notification) { @objc private func windowDidChangeScreen(notification: SwiftUI.Notification) {
guard let window = self.window else { return } guard let window = self.window else { return }
guard let object = notification.object as? NSWindow, window == object else { return } guard let object = notification.object as? NSWindow, window == object else { return }

View File

@ -318,7 +318,7 @@ pub fn keyEvent(
// Get the keybind entry for this event. We don't support key sequences // Get the keybind entry for this event. We don't support key sequences
// so we can look directly in the top-level set. // so we can look directly in the top-level set.
const entry = rt_app.config.keybind.set.getEvent(event) orelse return false; const entry = rt_app.config.keybind.set.getEvent(event) orelse return false;
const leaf: input.Binding.Set.Leaf = switch (entry) { const leaf: input.Binding.Set.Leaf = switch (entry.value_ptr.*) {
// Sequences aren't supported. Our configuration parser verifies // Sequences aren't supported. Our configuration parser verifies
// this for global keybinds but we may still get an entry for // this for global keybinds but we may still get an entry for
// a non-global keybind. // a non-global keybind.

View File

@ -1697,7 +1697,7 @@ fn maybeHandleBinding(
}; };
// Determine if this entry has an action or if its a leader key. // Determine if this entry has an action or if its a leader key.
const leaf: input.Binding.Set.Leaf = switch (entry) { const leaf: input.Binding.Set.Leaf = switch (entry.value_ptr.*) {
.leader => |set| { .leader => |set| {
// Setup the next set we'll look at. // Setup the next set we'll look at.
self.keyboard.bindings = set; self.keyboard.bindings = set;
@ -1709,6 +1709,18 @@ fn maybeHandleBinding(
try self.keyboard.queued.append(self.alloc, req); try self.keyboard.queued.append(self.alloc, req);
} }
// Start or continue our key sequence
self.rt_app.performAction(
.{ .surface = self },
.key_sequence,
.{ .trigger = entry.key_ptr.* },
) catch |err| {
log.warn(
"failed to notify app of key sequence err={}",
.{err},
);
};
return .consumed; return .consumed;
}, },
@ -1795,6 +1807,18 @@ fn endKeySequence(
action: KeySequenceQueued, action: KeySequenceQueued,
mem: KeySequenceMemory, mem: KeySequenceMemory,
) void { ) void {
// Notify apprt key sequence ended
self.rt_app.performAction(
.{ .surface = self },
.key_sequence,
.end,
) catch |err| {
log.warn(
"failed to notify app of key sequence end err={}",
.{err},
);
};
if (self.keyboard.queued.items.len > 0) { if (self.keyboard.queued.items.len > 0) {
switch (action) { switch (action) {
.flush => for (self.keyboard.queued.items) |write_req| { .flush => for (self.keyboard.queued.items) |write_req| {

View File

@ -1,6 +1,7 @@
const std = @import("std"); const std = @import("std");
const assert = std.debug.assert; const assert = std.debug.assert;
const apprt = @import("../apprt.zig"); const apprt = @import("../apprt.zig");
const input = @import("../input.zig");
const renderer = @import("../renderer.zig"); const renderer = @import("../renderer.zig");
const terminal = @import("../terminal/main.zig"); const terminal = @import("../terminal/main.zig");
const CoreSurface = @import("../Surface.zig"); const CoreSurface = @import("../Surface.zig");
@ -173,6 +174,11 @@ pub const Action = union(Key) {
/// system APIs to not log the input, etc. /// system APIs to not log the input, etc.
secure_input: SecureInput, secure_input: SecureInput,
/// A sequenced key binding has started, continued, or stopped.
/// The UI should show some indication that the user is in a sequenced
/// key mode because other input may be ignored.
key_sequence: KeySequence,
/// Sync with: ghostty_action_tag_e /// Sync with: ghostty_action_tag_e
pub const Key = enum(c_int) { pub const Key = enum(c_int) {
new_window, new_window,
@ -204,6 +210,7 @@ pub const Action = union(Key) {
open_config, open_config,
quit_timer, quit_timer,
secure_input, secure_input,
key_sequence,
}; };
/// Sync with: ghostty_action_u /// Sync with: ghostty_action_u
@ -411,3 +418,21 @@ pub const DesktopNotification = struct {
}; };
} }
}; };
pub const KeySequence = union(enum) {
trigger: input.Trigger,
end,
// Sync with: ghostty_action_key_sequence_s
pub const C = extern struct {
active: bool,
trigger: input.Trigger.C,
};
pub fn cval(self: KeySequence) C {
return switch (self) {
.trigger => |t| .{ .active = true, .trigger = t.cval() },
.end => .{ .active = false, .trigger = .{} },
};
}
};

View File

@ -203,6 +203,7 @@ pub const App = struct {
.render_inspector, .render_inspector,
.quit_timer, .quit_timer,
.secure_input, .secure_input,
.key_sequence,
.desktop_notification, .desktop_notification,
.mouse_over_link, .mouse_over_link,
.cell_size, .cell_size,

View File

@ -411,6 +411,7 @@ pub fn performAction(
.size_limit, .size_limit,
.cell_size, .cell_size,
.secure_input, .secure_input,
.key_sequence,
.render_inspector, .render_inspector,
.renderer_health, .renderer_health,
=> log.warn("unimplemented action={}", .{action}), => log.warn("unimplemented action={}", .{action}),

View File

@ -23,6 +23,7 @@ pub const MousePressureStage = mouse.PressureStage;
pub const ScrollMods = mouse.ScrollMods; pub const ScrollMods = mouse.ScrollMods;
pub const SplitFocusDirection = Binding.Action.SplitFocusDirection; pub const SplitFocusDirection = Binding.Action.SplitFocusDirection;
pub const SplitResizeDirection = Binding.Action.SplitResizeDirection; pub const SplitResizeDirection = Binding.Action.SplitResizeDirection;
pub const Trigger = Binding.Trigger;
// Keymap is only available on macOS right now. We could implement it // Keymap is only available on macOS right now. We could implement it
// in theory for XKB too on Linux but we don't need it right now. // in theory for XKB too on Linux but we don't need it right now.

View File

@ -1020,7 +1020,7 @@ pub const Trigger = struct {
pub const Set = struct { pub const Set = struct {
const HashMap = std.HashMapUnmanaged( const HashMap = std.HashMapUnmanaged(
Trigger, Trigger,
Entry, Value,
Context(Trigger), Context(Trigger),
std.hash_map.default_max_load_percentage, std.hash_map.default_max_load_percentage,
); );
@ -1046,7 +1046,7 @@ pub const Set = struct {
reverse: ReverseMap = .{}, reverse: ReverseMap = .{},
/// The entry type for the forward mapping of trigger to action. /// The entry type for the forward mapping of trigger to action.
pub const Entry = union(enum) { pub const Value = union(enum) {
/// This key is a leader key in a sequence. You must follow the given /// This key is a leader key in a sequence. You must follow the given
/// set to find the next key in the sequence. /// set to find the next key in the sequence.
leader: *Set, leader: *Set,
@ -1058,7 +1058,7 @@ pub const Set = struct {
/// Implements the formatter for the fmt package. This encodes the /// Implements the formatter for the fmt package. This encodes the
/// action back into the format used by parse. /// action back into the format used by parse.
pub fn format( pub fn format(
self: Entry, self: Value,
comptime layout: []const u8, comptime layout: []const u8,
opts: std.fmt.FormatOptions, opts: std.fmt.FormatOptions,
writer: anytype, writer: anytype,
@ -1100,6 +1100,9 @@ pub const Set = struct {
} }
}; };
/// A full key-value entry for the set.
pub const Entry = HashMap.Entry;
pub fn deinit(self: *Set, alloc: Allocator) void { pub fn deinit(self: *Set, alloc: Allocator) void {
// Clear any leaders if we have them // Clear any leaders if we have them
var it = self.bindings.iterator(); var it = self.bindings.iterator();
@ -1160,7 +1163,11 @@ pub const Set = struct {
switch (elem) { switch (elem) {
.leader => |t| { .leader => |t| {
// If we have a leader, we need to upsert a set for it. // If we have a leader, we need to upsert a set for it.
const old = set.get(t); // Since we remove the value, we need to copy it.
const old: ?Value = if (set.get(t)) |entry|
entry.value_ptr.*
else
null;
if (old) |entry| switch (entry) { if (old) |entry| switch (entry) {
// We have an existing leader for this key already // We have an existing leader for this key already
// so recurse into this set. // so recurse into this set.
@ -1289,7 +1296,7 @@ pub const Set = struct {
/// Get a binding for a given trigger. /// Get a binding for a given trigger.
pub fn get(self: Set, t: Trigger) ?Entry { pub fn get(self: Set, t: Trigger) ?Entry {
return self.bindings.get(t); return self.bindings.getEntry(t);
} }
/// Get a trigger for the given action. An action can have multiple /// Get a trigger for the given action. An action can have multiple
@ -1811,7 +1818,7 @@ test "set: parseAndPut typical binding" {
// Creates forward mapping // Creates forward mapping
{ {
const action = s.get(.{ .key = .{ .translated = .a } }).?.leaf; const action = s.get(.{ .key = .{ .translated = .a } }).?.value_ptr.*.leaf;
try testing.expect(action.action == .new_window); try testing.expect(action.action == .new_window);
try testing.expectEqual(Flags{}, action.flags); try testing.expectEqual(Flags{}, action.flags);
} }
@ -1835,7 +1842,7 @@ test "set: parseAndPut unconsumed binding" {
// Creates forward mapping // Creates forward mapping
{ {
const trigger: Trigger = .{ .key = .{ .translated = .a } }; const trigger: Trigger = .{ .key = .{ .translated = .a } };
const action = s.get(trigger).?.leaf; const action = s.get(trigger).?.value_ptr.*.leaf;
try testing.expect(action.action == .new_window); try testing.expect(action.action == .new_window);
try testing.expectEqual(Flags{ .consumed = false }, action.flags); try testing.expectEqual(Flags{ .consumed = false }, action.flags);
} }
@ -1876,13 +1883,13 @@ test "set: parseAndPut sequence" {
var current: *Set = &s; var current: *Set = &s;
{ {
const t: Trigger = .{ .key = .{ .translated = .a } }; const t: Trigger = .{ .key = .{ .translated = .a } };
const e = current.get(t).?; const e = current.get(t).?.value_ptr.*;
try testing.expect(e == .leader); try testing.expect(e == .leader);
current = e.leader; current = e.leader;
} }
{ {
const t: Trigger = .{ .key = .{ .translated = .b } }; const t: Trigger = .{ .key = .{ .translated = .b } };
const e = current.get(t).?; const e = current.get(t).?.value_ptr.*;
try testing.expect(e == .leaf); try testing.expect(e == .leaf);
try testing.expect(e.leaf.action == .new_window); try testing.expect(e.leaf.action == .new_window);
try testing.expectEqual(Flags{}, e.leaf.flags); try testing.expectEqual(Flags{}, e.leaf.flags);
@ -1901,20 +1908,20 @@ test "set: parseAndPut sequence with two actions" {
var current: *Set = &s; var current: *Set = &s;
{ {
const t: Trigger = .{ .key = .{ .translated = .a } }; const t: Trigger = .{ .key = .{ .translated = .a } };
const e = current.get(t).?; const e = current.get(t).?.value_ptr.*;
try testing.expect(e == .leader); try testing.expect(e == .leader);
current = e.leader; current = e.leader;
} }
{ {
const t: Trigger = .{ .key = .{ .translated = .b } }; const t: Trigger = .{ .key = .{ .translated = .b } };
const e = current.get(t).?; const e = current.get(t).?.value_ptr.*;
try testing.expect(e == .leaf); try testing.expect(e == .leaf);
try testing.expect(e.leaf.action == .new_window); try testing.expect(e.leaf.action == .new_window);
try testing.expectEqual(Flags{}, e.leaf.flags); try testing.expectEqual(Flags{}, e.leaf.flags);
} }
{ {
const t: Trigger = .{ .key = .{ .translated = .c } }; const t: Trigger = .{ .key = .{ .translated = .c } };
const e = current.get(t).?; const e = current.get(t).?.value_ptr.*;
try testing.expect(e == .leaf); try testing.expect(e == .leaf);
try testing.expect(e.leaf.action == .new_tab); try testing.expect(e.leaf.action == .new_tab);
try testing.expectEqual(Flags{}, e.leaf.flags); try testing.expectEqual(Flags{}, e.leaf.flags);
@ -1933,13 +1940,13 @@ test "set: parseAndPut overwrite sequence" {
var current: *Set = &s; var current: *Set = &s;
{ {
const t: Trigger = .{ .key = .{ .translated = .a } }; const t: Trigger = .{ .key = .{ .translated = .a } };
const e = current.get(t).?; const e = current.get(t).?.value_ptr.*;
try testing.expect(e == .leader); try testing.expect(e == .leader);
current = e.leader; current = e.leader;
} }
{ {
const t: Trigger = .{ .key = .{ .translated = .b } }; const t: Trigger = .{ .key = .{ .translated = .b } };
const e = current.get(t).?; const e = current.get(t).?.value_ptr.*;
try testing.expect(e == .leaf); try testing.expect(e == .leaf);
try testing.expect(e.leaf.action == .new_window); try testing.expect(e.leaf.action == .new_window);
try testing.expectEqual(Flags{}, e.leaf.flags); try testing.expectEqual(Flags{}, e.leaf.flags);
@ -1958,13 +1965,13 @@ test "set: parseAndPut overwrite leader" {
var current: *Set = &s; var current: *Set = &s;
{ {
const t: Trigger = .{ .key = .{ .translated = .a } }; const t: Trigger = .{ .key = .{ .translated = .a } };
const e = current.get(t).?; const e = current.get(t).?.value_ptr.*;
try testing.expect(e == .leader); try testing.expect(e == .leader);
current = e.leader; current = e.leader;
} }
{ {
const t: Trigger = .{ .key = .{ .translated = .b } }; const t: Trigger = .{ .key = .{ .translated = .b } };
const e = current.get(t).?; const e = current.get(t).?.value_ptr.*;
try testing.expect(e == .leaf); try testing.expect(e == .leaf);
try testing.expect(e.leaf.action == .new_window); try testing.expect(e.leaf.action == .new_window);
try testing.expectEqual(Flags{}, e.leaf.flags); try testing.expectEqual(Flags{}, e.leaf.flags);
@ -2096,8 +2103,8 @@ test "set: consumed state" {
defer s.deinit(alloc); defer s.deinit(alloc);
try s.put(alloc, .{ .key = .{ .translated = .a } }, .{ .new_window = {} }); try s.put(alloc, .{ .key = .{ .translated = .a } }, .{ .new_window = {} });
try testing.expect(s.get(.{ .key = .{ .translated = .a } }).? == .leaf); try testing.expect(s.get(.{ .key = .{ .translated = .a } }).?.value_ptr.* == .leaf);
try testing.expect(s.get(.{ .key = .{ .translated = .a } }).?.leaf.flags.consumed); try testing.expect(s.get(.{ .key = .{ .translated = .a } }).?.value_ptr.*.leaf.flags.consumed);
try s.putFlags( try s.putFlags(
alloc, alloc,
@ -2105,10 +2112,10 @@ test "set: consumed state" {
.{ .new_window = {} }, .{ .new_window = {} },
.{ .consumed = false }, .{ .consumed = false },
); );
try testing.expect(s.get(.{ .key = .{ .translated = .a } }).? == .leaf); try testing.expect(s.get(.{ .key = .{ .translated = .a } }).?.value_ptr.* == .leaf);
try testing.expect(!s.get(.{ .key = .{ .translated = .a } }).?.leaf.flags.consumed); try testing.expect(!s.get(.{ .key = .{ .translated = .a } }).?.value_ptr.*.leaf.flags.consumed);
try s.put(alloc, .{ .key = .{ .translated = .a } }, .{ .new_window = {} }); try s.put(alloc, .{ .key = .{ .translated = .a } }, .{ .new_window = {} });
try testing.expect(s.get(.{ .key = .{ .translated = .a } }).? == .leaf); try testing.expect(s.get(.{ .key = .{ .translated = .a } }).?.value_ptr.* == .leaf);
try testing.expect(s.get(.{ .key = .{ .translated = .a } }).?.leaf.flags.consumed); try testing.expect(s.get(.{ .key = .{ .translated = .a } }).?.value_ptr.*.leaf.flags.consumed);
} }