Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

performable: prefix #4345

Merged
merged 5 commits into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions src/Surface.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1156,7 +1156,6 @@ pub fn updateConfig(
}

// If we are in the middle of a key sequence, clear it.
self.keyboard.bindings = null;
self.endKeySequence(.drop, .free);

// Before sending any other config changes, we give the renderer a new font
Expand Down Expand Up @@ -1853,9 +1852,6 @@ fn maybeHandleBinding(
if (self.keyboard.bindings != null and
!event.key.modifier())
{
// Reset to the root set
self.keyboard.bindings = null;

// Encode everything up to this point
self.endKeySequence(.flush, .retain);
}
Expand Down Expand Up @@ -1941,10 +1937,21 @@ fn maybeHandleBinding(
return .closed;
}

// If we have the performable flag and the action was not performed,
// then we act as though a binding didn't exist.
if (leaf.flags.performable and !performed) {
// If we're in a sequence, we treat this as if we pressed a key
// that doesn't exist in the sequence. Reset our sequence and flush
// any queued events.
self.endKeySequence(.flush, .retain);

return null;
}

// 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) {
if (consumed) {
// If we had queued events, we deinit them since we consumed
self.endKeySequence(.drop, .retain);

Expand Down Expand Up @@ -1986,6 +1993,10 @@ fn endKeySequence(
);
};

// No matter what we clear our current binding set. This restores
// the set we look at to the root set.
self.keyboard.bindings = null;

if (self.keyboard.queued.items.len > 0) {
switch (action) {
.flush => for (self.keyboard.queued.items) |write_req| {
Expand Down Expand Up @@ -3889,7 +3900,11 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
log.err("error setting clipboard string err={}", .{err});
return true;
};

return true;
}

return false;
},

.paste_from_clipboard => try self.startClipboardRequest(
Expand Down
36 changes: 27 additions & 9 deletions src/config/Config.zig
Original file line number Diff line number Diff line change
Expand Up @@ -907,6 +907,15 @@ class: ?[:0]const u8 = null,
/// Since they are not associated with a specific terminal surface,
/// they're never encoded.
///
/// * `performable:` - Only consume the input if the action is able to be
/// performed. For example, the `copy_to_clipboard` action will only
/// consume the input if there is a selection to copy. If there is no
/// selection, Ghostty behaves as if the keybind was not set. This has
/// no effect with `global:` or `all:`-prefixed keybinds. For key
/// sequences, this will reset the sequence if the action is not
/// performable (acting identically to not having a keybind set at
/// all).
///
/// Keybind triggers are not unique per prefix combination. For example,
/// `ctrl+a` and `global:ctrl+a` are not two separate keybinds. The keybind
/// set later will overwrite the keybind set earlier. In this case, the
Expand Down Expand Up @@ -2124,45 +2133,53 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
);

// Expand Selection
try result.keybind.set.put(
try result.keybind.set.putFlags(
alloc,
.{ .key = .{ .translated = .left }, .mods = .{ .shift = true } },
.{ .adjust_selection = .left },
.{ .performable = true },
);
try result.keybind.set.put(
try result.keybind.set.putFlags(
alloc,
.{ .key = .{ .translated = .right }, .mods = .{ .shift = true } },
.{ .adjust_selection = .right },
.{ .performable = true },
);
try result.keybind.set.put(
try result.keybind.set.putFlags(
alloc,
.{ .key = .{ .translated = .up }, .mods = .{ .shift = true } },
.{ .adjust_selection = .up },
.{ .performable = true },
);
try result.keybind.set.put(
try result.keybind.set.putFlags(
alloc,
.{ .key = .{ .translated = .down }, .mods = .{ .shift = true } },
.{ .adjust_selection = .down },
.{ .performable = true },
);
try result.keybind.set.put(
try result.keybind.set.putFlags(
alloc,
.{ .key = .{ .translated = .page_up }, .mods = .{ .shift = true } },
.{ .adjust_selection = .page_up },
.{ .performable = true },
);
try result.keybind.set.put(
try result.keybind.set.putFlags(
alloc,
.{ .key = .{ .translated = .page_down }, .mods = .{ .shift = true } },
.{ .adjust_selection = .page_down },
.{ .performable = true },
);
try result.keybind.set.put(
try result.keybind.set.putFlags(
alloc,
.{ .key = .{ .translated = .home }, .mods = .{ .shift = true } },
.{ .adjust_selection = .home },
.{ .performable = true },
);
try result.keybind.set.put(
try result.keybind.set.putFlags(
alloc,
.{ .key = .{ .translated = .end }, .mods = .{ .shift = true } },
.{ .adjust_selection = .end },
.{ .performable = true },
);

// Tabs common to all platforms
Expand Down Expand Up @@ -2412,10 +2429,11 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
.{ .key = .{ .translated = .q }, .mods = .{ .super = true } },
.{ .quit = {} },
);
try result.keybind.set.put(
try result.keybind.set.putFlags(
alloc,
.{ .key = .{ .translated = .k }, .mods = .{ .super = true } },
.{ .clear_screen = {} },
.{ .performable = true },
);
try result.keybind.set.put(
alloc,
Expand Down
18 changes: 18 additions & 0 deletions src/input/Binding.zig
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ pub const Flags = packed struct {
/// and not just while Ghostty is focused. This may not work on all platforms.
/// See the keybind config documentation for more information.
global: bool = false,

/// True if this binding should only be triggered if the action can be
/// performed. If the action can't be performed then the binding acts as
/// if it doesn't exist.
performable: bool = false,
};

/// Full binding parser. The binding parser is implemented as an iterator
Expand Down Expand Up @@ -90,6 +95,9 @@ pub const Parser = struct {
} else if (std.mem.eql(u8, prefix, "unconsumed")) {
if (!flags.consumed) return Error.InvalidFormat;
flags.consumed = false;
} else if (std.mem.eql(u8, prefix, "performable")) {
if (flags.performable) return Error.InvalidFormat;
flags.performable = true;
} else {
// If we don't recognize the prefix then we're done.
// There are trigger-specific prefixes like "physical:" so
Expand Down Expand Up @@ -1647,6 +1655,16 @@ test "parse: triggers" {
.flags = .{ .consumed = false },
}, try parseSingle("unconsumed:physical:a+shift=ignore"));

// performable keys
try testing.expectEqual(Binding{
.trigger = .{
.mods = .{ .shift = true },
.key = .{ .translated = .a },
},
.action = .{ .ignore = {} },
.flags = .{ .performable = true },
}, try parseSingle("performable:shift+a=ignore"));

// invalid key
try testing.expectError(Error.InvalidFormat, parseSingle("foo=ignore"));

Expand Down