mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 16:26:08 +03:00
Merge pull request #2418 from ghostty-org/apprt-key-seq
macOS: Key Sequence UI
This commit is contained in:
@ -500,6 +500,12 @@ typedef enum {
|
||||
GHOSTTY_RENDERER_HEALTH_UNHEALTHY,
|
||||
} 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
|
||||
typedef enum {
|
||||
GHOSTTY_ACTION_NEW_WINDOW,
|
||||
@ -531,6 +537,7 @@ typedef enum {
|
||||
GHOSTTY_ACTION_OPEN_CONFIG,
|
||||
GHOSTTY_ACTION_QUIT_TIMER,
|
||||
GHOSTTY_ACTION_SECURE_INPUT,
|
||||
GHOSTTY_ACTION_KEY_SEQUENCE,
|
||||
} ghostty_action_tag_e;
|
||||
|
||||
typedef union {
|
||||
@ -551,6 +558,7 @@ typedef union {
|
||||
ghostty_action_renderer_health_e renderer_health;
|
||||
ghostty_action_quit_timer_e quit_timer;
|
||||
ghostty_action_secure_input_e secure_input;
|
||||
ghostty_action_key_sequence_s key_sequence;
|
||||
} ghostty_action_u;
|
||||
|
||||
typedef struct {
|
||||
|
@ -515,6 +515,9 @@ extension Ghostty {
|
||||
case GHOSTTY_ACTION_TOGGLE_VISIBILITY:
|
||||
toggleVisibility(app, target: target)
|
||||
|
||||
case GHOSTTY_ACTION_KEY_SEQUENCE:
|
||||
keySequence(app, target: target, v: action.action.key_sequence)
|
||||
|
||||
case GHOSTTY_ACTION_CLOSE_ALL_WINDOWS:
|
||||
fallthrough
|
||||
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
|
||||
|
||||
/// Handle a received user notification. This is called when a user notification is clicked or dismissed by the user
|
||||
|
@ -87,25 +87,6 @@ extension Ghostty {
|
||||
#if os(macOS)
|
||||
// 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
|
||||
/// in the Ghostty configuration. For example `keybind = cmd+q=quit` in Ghostty
|
||||
/// configuration would be "quit" action.
|
||||
@ -115,33 +96,7 @@ extension Ghostty {
|
||||
guard let cfg = self.config else { return nil }
|
||||
|
||||
let trigger = ghostty_config_trigger(cfg, action, UInt(action.count))
|
||||
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)
|
||||
)
|
||||
return Ghostty.keyEquivalent(for: trigger)
|
||||
}
|
||||
#endif
|
||||
|
||||
|
@ -2,11 +2,68 @@ import Cocoa
|
||||
import GhosttyKit
|
||||
|
||||
extension Ghostty {
|
||||
// MARK: Key Equivalents
|
||||
|
||||
/// 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? {
|
||||
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.
|
||||
static func eventModifierFlags(mods: ghostty_input_mods_e) -> NSEvent.ModifierFlags {
|
||||
var flags = NSEvent.ModifierFlags(rawValue: 0);
|
||||
|
@ -262,7 +262,12 @@ extension Ghostty.Notification {
|
||||
|
||||
/// Notification that renderer health changed
|
||||
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.
|
||||
extension ghostty_input_key_e : Hashable {}
|
||||
extension ghostty_input_key_e : @retroactive Hashable {}
|
||||
|
@ -184,6 +184,34 @@ extension Ghostty {
|
||||
}
|
||||
.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 let url = surfaceView.hoverUrl {
|
||||
let padding: CGFloat = 3
|
||||
|
@ -30,6 +30,9 @@ extension Ghostty {
|
||||
// The hovered URL string
|
||||
@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
|
||||
// on supported platforms.
|
||||
@Published var focusInstant: Any? = nil
|
||||
@ -132,6 +135,16 @@ extension Ghostty {
|
||||
selector: #selector(onUpdateRendererHealth),
|
||||
name: Ghostty.Notification.didUpdateRendererHealth,
|
||||
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(
|
||||
self,
|
||||
selector: #selector(windowDidChangeScreen),
|
||||
@ -316,6 +329,16 @@ extension Ghostty {
|
||||
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) {
|
||||
guard let window = self.window else { return }
|
||||
guard let object = notification.object as? NSWindow, window == object else { return }
|
||||
|
@ -318,7 +318,7 @@ pub fn keyEvent(
|
||||
// Get the keybind entry for this event. We don't support key sequences
|
||||
// so we can look directly in the top-level set.
|
||||
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
|
||||
// this for global keybinds but we may still get an entry for
|
||||
// a non-global keybind.
|
||||
|
@ -1697,7 +1697,7 @@ fn maybeHandleBinding(
|
||||
};
|
||||
|
||||
// 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| {
|
||||
// Setup the next set we'll look at.
|
||||
self.keyboard.bindings = set;
|
||||
@ -1709,6 +1709,18 @@ fn maybeHandleBinding(
|
||||
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;
|
||||
},
|
||||
|
||||
@ -1795,6 +1807,18 @@ fn endKeySequence(
|
||||
action: KeySequenceQueued,
|
||||
mem: KeySequenceMemory,
|
||||
) 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) {
|
||||
switch (action) {
|
||||
.flush => for (self.keyboard.queued.items) |write_req| {
|
||||
|
@ -1,6 +1,7 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const apprt = @import("../apprt.zig");
|
||||
const input = @import("../input.zig");
|
||||
const renderer = @import("../renderer.zig");
|
||||
const terminal = @import("../terminal/main.zig");
|
||||
const CoreSurface = @import("../Surface.zig");
|
||||
@ -173,6 +174,11 @@ pub const Action = union(Key) {
|
||||
/// system APIs to not log the input, etc.
|
||||
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
|
||||
pub const Key = enum(c_int) {
|
||||
new_window,
|
||||
@ -204,6 +210,7 @@ pub const Action = union(Key) {
|
||||
open_config,
|
||||
quit_timer,
|
||||
secure_input,
|
||||
key_sequence,
|
||||
};
|
||||
|
||||
/// 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 = .{} },
|
||||
};
|
||||
}
|
||||
};
|
||||
|
@ -203,6 +203,7 @@ pub const App = struct {
|
||||
.render_inspector,
|
||||
.quit_timer,
|
||||
.secure_input,
|
||||
.key_sequence,
|
||||
.desktop_notification,
|
||||
.mouse_over_link,
|
||||
.cell_size,
|
||||
|
@ -411,6 +411,7 @@ pub fn performAction(
|
||||
.size_limit,
|
||||
.cell_size,
|
||||
.secure_input,
|
||||
.key_sequence,
|
||||
.render_inspector,
|
||||
.renderer_health,
|
||||
=> log.warn("unimplemented action={}", .{action}),
|
||||
|
@ -23,6 +23,7 @@ pub const MousePressureStage = mouse.PressureStage;
|
||||
pub const ScrollMods = mouse.ScrollMods;
|
||||
pub const SplitFocusDirection = Binding.Action.SplitFocusDirection;
|
||||
pub const SplitResizeDirection = Binding.Action.SplitResizeDirection;
|
||||
pub const Trigger = Binding.Trigger;
|
||||
|
||||
// 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.
|
||||
|
@ -1020,7 +1020,7 @@ pub const Trigger = struct {
|
||||
pub const Set = struct {
|
||||
const HashMap = std.HashMapUnmanaged(
|
||||
Trigger,
|
||||
Entry,
|
||||
Value,
|
||||
Context(Trigger),
|
||||
std.hash_map.default_max_load_percentage,
|
||||
);
|
||||
@ -1046,7 +1046,7 @@ pub const Set = struct {
|
||||
reverse: ReverseMap = .{},
|
||||
|
||||
/// 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
|
||||
/// set to find the next key in the sequence.
|
||||
leader: *Set,
|
||||
@ -1058,7 +1058,7 @@ pub const Set = struct {
|
||||
/// Implements the formatter for the fmt package. This encodes the
|
||||
/// action back into the format used by parse.
|
||||
pub fn format(
|
||||
self: Entry,
|
||||
self: Value,
|
||||
comptime layout: []const u8,
|
||||
opts: std.fmt.FormatOptions,
|
||||
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 {
|
||||
// Clear any leaders if we have them
|
||||
var it = self.bindings.iterator();
|
||||
@ -1160,7 +1163,11 @@ pub const Set = struct {
|
||||
switch (elem) {
|
||||
.leader => |t| {
|
||||
// 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) {
|
||||
// We have an existing leader for this key already
|
||||
// so recurse into this set.
|
||||
@ -1289,7 +1296,7 @@ pub const Set = struct {
|
||||
|
||||
/// Get a binding for a given trigger.
|
||||
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
|
||||
@ -1811,7 +1818,7 @@ test "set: parseAndPut typical binding" {
|
||||
|
||||
// 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.expectEqual(Flags{}, action.flags);
|
||||
}
|
||||
@ -1835,7 +1842,7 @@ test "set: parseAndPut unconsumed binding" {
|
||||
// Creates forward mapping
|
||||
{
|
||||
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.expectEqual(Flags{ .consumed = false }, action.flags);
|
||||
}
|
||||
@ -1876,13 +1883,13 @@ test "set: parseAndPut sequence" {
|
||||
var current: *Set = &s;
|
||||
{
|
||||
const t: Trigger = .{ .key = .{ .translated = .a } };
|
||||
const e = current.get(t).?;
|
||||
const e = current.get(t).?.value_ptr.*;
|
||||
try testing.expect(e == .leader);
|
||||
current = e.leader;
|
||||
}
|
||||
{
|
||||
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.action == .new_window);
|
||||
try testing.expectEqual(Flags{}, e.leaf.flags);
|
||||
@ -1901,20 +1908,20 @@ test "set: parseAndPut sequence with two actions" {
|
||||
var current: *Set = &s;
|
||||
{
|
||||
const t: Trigger = .{ .key = .{ .translated = .a } };
|
||||
const e = current.get(t).?;
|
||||
const e = current.get(t).?.value_ptr.*;
|
||||
try testing.expect(e == .leader);
|
||||
current = e.leader;
|
||||
}
|
||||
{
|
||||
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.action == .new_window);
|
||||
try testing.expectEqual(Flags{}, e.leaf.flags);
|
||||
}
|
||||
{
|
||||
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.action == .new_tab);
|
||||
try testing.expectEqual(Flags{}, e.leaf.flags);
|
||||
@ -1933,13 +1940,13 @@ test "set: parseAndPut overwrite sequence" {
|
||||
var current: *Set = &s;
|
||||
{
|
||||
const t: Trigger = .{ .key = .{ .translated = .a } };
|
||||
const e = current.get(t).?;
|
||||
const e = current.get(t).?.value_ptr.*;
|
||||
try testing.expect(e == .leader);
|
||||
current = e.leader;
|
||||
}
|
||||
{
|
||||
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.action == .new_window);
|
||||
try testing.expectEqual(Flags{}, e.leaf.flags);
|
||||
@ -1958,13 +1965,13 @@ test "set: parseAndPut overwrite leader" {
|
||||
var current: *Set = &s;
|
||||
{
|
||||
const t: Trigger = .{ .key = .{ .translated = .a } };
|
||||
const e = current.get(t).?;
|
||||
const e = current.get(t).?.value_ptr.*;
|
||||
try testing.expect(e == .leader);
|
||||
current = e.leader;
|
||||
}
|
||||
{
|
||||
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.action == .new_window);
|
||||
try testing.expectEqual(Flags{}, e.leaf.flags);
|
||||
@ -2096,8 +2103,8 @@ test "set: consumed state" {
|
||||
defer s.deinit(alloc);
|
||||
|
||||
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 } }).?.leaf.flags.consumed);
|
||||
try testing.expect(s.get(.{ .key = .{ .translated = .a } }).?.value_ptr.* == .leaf);
|
||||
try testing.expect(s.get(.{ .key = .{ .translated = .a } }).?.value_ptr.*.leaf.flags.consumed);
|
||||
|
||||
try s.putFlags(
|
||||
alloc,
|
||||
@ -2105,10 +2112,10 @@ test "set: consumed state" {
|
||||
.{ .new_window = {} },
|
||||
.{ .consumed = false },
|
||||
);
|
||||
try testing.expect(s.get(.{ .key = .{ .translated = .a } }).? == .leaf);
|
||||
try testing.expect(!s.get(.{ .key = .{ .translated = .a } }).?.leaf.flags.consumed);
|
||||
try testing.expect(s.get(.{ .key = .{ .translated = .a } }).?.value_ptr.* == .leaf);
|
||||
try testing.expect(!s.get(.{ .key = .{ .translated = .a } }).?.value_ptr.*.leaf.flags.consumed);
|
||||
|
||||
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 } }).?.leaf.flags.consumed);
|
||||
try testing.expect(s.get(.{ .key = .{ .translated = .a } }).?.value_ptr.* == .leaf);
|
||||
try testing.expect(s.get(.{ .key = .{ .translated = .a } }).?.value_ptr.*.leaf.flags.consumed);
|
||||
}
|
||||
|
Reference in New Issue
Block a user