mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 00:36:07 +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_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 {
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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 {}
|
||||||
|
@ -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
|
||||||
|
@ -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 }
|
||||||
|
@ -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.
|
||||||
|
@ -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| {
|
||||||
|
@ -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 = .{} },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -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,
|
||||||
|
@ -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}),
|
||||||
|
@ -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.
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user