From 46097617b4e7b03256a96ee39d80856998eae8ac Mon Sep 17 00:00:00 2001 From: Ethan Conneely Date: Thu, 2 Jan 2025 00:18:05 +0000 Subject: [PATCH 1/5] copy_to_clipboard return false if not performed --- src/Surface.zig | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Surface.zig b/src/Surface.zig index c359efd8ad..3677c04e53 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -3889,7 +3889,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( From 98aa046a4dbd1a27953a9587eca078783d0db15f Mon Sep 17 00:00:00 2001 From: Ethan Conneely Date: Thu, 2 Jan 2025 00:18:39 +0000 Subject: [PATCH 2/5] Add performable flag --- src/input/Binding.zig | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/input/Binding.zig b/src/input/Binding.zig index b2c03b6743..3380896b45 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -36,6 +36,10 @@ 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 can be performed then the action is + /// triggered otherwise it acts as if it doesn't exist. + performable: bool = false, }; /// Full binding parser. The binding parser is implemented as an iterator @@ -90,6 +94,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 From f38d1585e8c739aae1e0e1513118295671aafb2f Mon Sep 17 00:00:00 2001 From: Ethan Conneely Date: Thu, 2 Jan 2025 01:14:47 +0000 Subject: [PATCH 3/5] Do nothing if action not performed with flag --- src/Surface.zig | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Surface.zig b/src/Surface.zig index 3677c04e53..ce70d56ff7 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1941,10 +1941,16 @@ fn maybeHandleBinding( return .closed; } + // If we have the performable flag and the + // action was not performed do nothing at all + if (leaf.flags.performable and !performed) { + 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); From 95b73f197fbfccc15c4635630e285cb5a4acbb69 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 2 Jan 2025 15:38:41 -0800 Subject: [PATCH 4/5] Add docs for performable --- src/Surface.zig | 14 +++++++++++--- src/config/Config.zig | 9 +++++++++ src/input/Binding.zig | 15 +++++++++++++-- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index ce70d56ff7..5d25a61e9e 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1941,16 +1941,24 @@ fn maybeHandleBinding( return .closed; } - // If we have the performable flag and the - // action was not performed do nothing at all + // 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. + if (self.keyboard.bindings != null) { + self.keyboard.bindings = null; + 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 (consumed) { + if (performed and consumed) { // If we had queued events, we deinit them since we consumed self.endKeySequence(.drop, .retain); diff --git a/src/config/Config.zig b/src/config/Config.zig index dca3bec0d9..9692caae14 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -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 diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 3380896b45..529ca1902a 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -37,8 +37,9 @@ pub const Flags = packed struct { /// See the keybind config documentation for more information. global: bool = false, - /// True if this binding can be performed then the action is - /// triggered otherwise it acts as if it doesn't exist. + /// 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, }; @@ -1654,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")); From e6399c947a7c6e3d151985bdc32dc15f78045bdb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 2 Jan 2025 15:49:35 -0800 Subject: [PATCH 5/5] update our default bindings that are performable --- src/Surface.zig | 15 ++++++--------- src/config/Config.zig | 27 ++++++++++++++++++--------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 5d25a61e9e..389e7f7e46 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -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 @@ -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); } @@ -1947,10 +1943,7 @@ fn maybeHandleBinding( // 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. - if (self.keyboard.bindings != null) { - self.keyboard.bindings = null; - self.endKeySequence(.flush, .retain); - } + self.endKeySequence(.flush, .retain); return null; } @@ -1958,7 +1951,7 @@ fn maybeHandleBinding( // 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); @@ -2000,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| { diff --git a/src/config/Config.zig b/src/config/Config.zig index 9692caae14..1f136b227d 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2133,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 @@ -2421,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,