mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
performable: prefix (#4345)
closes #4328 closes #3970 makes this possible now ``` keybind = performable:ctrl+c=copy_to_clipboard # copy if theres a selection else send sigint keybind = ctrl+v=paste_from_clipboard ```
This commit is contained in:
@ -1156,7 +1156,6 @@ pub fn updateConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If we are in the middle of a key sequence, clear it.
|
// If we are in the middle of a key sequence, clear it.
|
||||||
self.keyboard.bindings = null;
|
|
||||||
self.endKeySequence(.drop, .free);
|
self.endKeySequence(.drop, .free);
|
||||||
|
|
||||||
// Before sending any other config changes, we give the renderer a new font
|
// 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
|
if (self.keyboard.bindings != null and
|
||||||
!event.key.modifier())
|
!event.key.modifier())
|
||||||
{
|
{
|
||||||
// Reset to the root set
|
|
||||||
self.keyboard.bindings = null;
|
|
||||||
|
|
||||||
// Encode everything up to this point
|
// Encode everything up to this point
|
||||||
self.endKeySequence(.flush, .retain);
|
self.endKeySequence(.flush, .retain);
|
||||||
}
|
}
|
||||||
@ -1941,10 +1937,21 @@ fn maybeHandleBinding(
|
|||||||
return .closed;
|
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
|
// 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
|
// it, we processed the action but we still want to process our
|
||||||
// encodings, too.
|
// encodings, too.
|
||||||
if (performed and consumed) {
|
if (consumed) {
|
||||||
// If we had queued events, we deinit them since we consumed
|
// If we had queued events, we deinit them since we consumed
|
||||||
self.endKeySequence(.drop, .retain);
|
self.endKeySequence(.drop, .retain);
|
||||||
|
|
||||||
@ -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) {
|
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| {
|
||||||
@ -3889,7 +3900,11 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
|||||||
log.err("error setting clipboard string err={}", .{err});
|
log.err("error setting clipboard string err={}", .{err});
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
.paste_from_clipboard => try self.startClipboardRequest(
|
.paste_from_clipboard => try self.startClipboardRequest(
|
||||||
|
@ -929,6 +929,15 @@ class: ?[:0]const u8 = null,
|
|||||||
/// Since they are not associated with a specific terminal surface,
|
/// Since they are not associated with a specific terminal surface,
|
||||||
/// they're never encoded.
|
/// 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,
|
/// Keybind triggers are not unique per prefix combination. For example,
|
||||||
/// `ctrl+a` and `global:ctrl+a` are not two separate keybinds. The keybind
|
/// `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
|
/// set later will overwrite the keybind set earlier. In this case, the
|
||||||
@ -2221,45 +2230,53 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Expand Selection
|
// Expand Selection
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.putFlags(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .{ .translated = .left }, .mods = .{ .shift = true } },
|
.{ .key = .{ .translated = .left }, .mods = .{ .shift = true } },
|
||||||
.{ .adjust_selection = .left },
|
.{ .adjust_selection = .left },
|
||||||
|
.{ .performable = true },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.putFlags(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .{ .translated = .right }, .mods = .{ .shift = true } },
|
.{ .key = .{ .translated = .right }, .mods = .{ .shift = true } },
|
||||||
.{ .adjust_selection = .right },
|
.{ .adjust_selection = .right },
|
||||||
|
.{ .performable = true },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.putFlags(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .{ .translated = .up }, .mods = .{ .shift = true } },
|
.{ .key = .{ .translated = .up }, .mods = .{ .shift = true } },
|
||||||
.{ .adjust_selection = .up },
|
.{ .adjust_selection = .up },
|
||||||
|
.{ .performable = true },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.putFlags(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .{ .translated = .down }, .mods = .{ .shift = true } },
|
.{ .key = .{ .translated = .down }, .mods = .{ .shift = true } },
|
||||||
.{ .adjust_selection = .down },
|
.{ .adjust_selection = .down },
|
||||||
|
.{ .performable = true },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.putFlags(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .{ .translated = .page_up }, .mods = .{ .shift = true } },
|
.{ .key = .{ .translated = .page_up }, .mods = .{ .shift = true } },
|
||||||
.{ .adjust_selection = .page_up },
|
.{ .adjust_selection = .page_up },
|
||||||
|
.{ .performable = true },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.putFlags(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .{ .translated = .page_down }, .mods = .{ .shift = true } },
|
.{ .key = .{ .translated = .page_down }, .mods = .{ .shift = true } },
|
||||||
.{ .adjust_selection = .page_down },
|
.{ .adjust_selection = .page_down },
|
||||||
|
.{ .performable = true },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.putFlags(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .{ .translated = .home }, .mods = .{ .shift = true } },
|
.{ .key = .{ .translated = .home }, .mods = .{ .shift = true } },
|
||||||
.{ .adjust_selection = .home },
|
.{ .adjust_selection = .home },
|
||||||
|
.{ .performable = true },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.putFlags(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .{ .translated = .end }, .mods = .{ .shift = true } },
|
.{ .key = .{ .translated = .end }, .mods = .{ .shift = true } },
|
||||||
.{ .adjust_selection = .end },
|
.{ .adjust_selection = .end },
|
||||||
|
.{ .performable = true },
|
||||||
);
|
);
|
||||||
|
|
||||||
// Tabs common to all platforms
|
// Tabs common to all platforms
|
||||||
@ -2509,10 +2526,11 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
|
|||||||
.{ .key = .{ .translated = .q }, .mods = .{ .super = true } },
|
.{ .key = .{ .translated = .q }, .mods = .{ .super = true } },
|
||||||
.{ .quit = {} },
|
.{ .quit = {} },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.putFlags(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .{ .translated = .k }, .mods = .{ .super = true } },
|
.{ .key = .{ .translated = .k }, .mods = .{ .super = true } },
|
||||||
.{ .clear_screen = {} },
|
.{ .clear_screen = {} },
|
||||||
|
.{ .performable = true },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
|
@ -36,6 +36,11 @@ pub const Flags = packed struct {
|
|||||||
/// and not just while Ghostty is focused. This may not work on all platforms.
|
/// and not just while Ghostty is focused. This may not work on all platforms.
|
||||||
/// See the keybind config documentation for more information.
|
/// See the keybind config documentation for more information.
|
||||||
global: bool = false,
|
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
|
/// Full binding parser. The binding parser is implemented as an iterator
|
||||||
@ -90,6 +95,9 @@ pub const Parser = struct {
|
|||||||
} else if (std.mem.eql(u8, prefix, "unconsumed")) {
|
} else if (std.mem.eql(u8, prefix, "unconsumed")) {
|
||||||
if (!flags.consumed) return Error.InvalidFormat;
|
if (!flags.consumed) return Error.InvalidFormat;
|
||||||
flags.consumed = false;
|
flags.consumed = false;
|
||||||
|
} else if (std.mem.eql(u8, prefix, "performable")) {
|
||||||
|
if (flags.performable) return Error.InvalidFormat;
|
||||||
|
flags.performable = true;
|
||||||
} else {
|
} else {
|
||||||
// If we don't recognize the prefix then we're done.
|
// If we don't recognize the prefix then we're done.
|
||||||
// There are trigger-specific prefixes like "physical:" so
|
// There are trigger-specific prefixes like "physical:" so
|
||||||
@ -1688,6 +1696,16 @@ test "parse: triggers" {
|
|||||||
.flags = .{ .consumed = false },
|
.flags = .{ .consumed = false },
|
||||||
}, try parseSingle("unconsumed:physical:a+shift=ignore"));
|
}, 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
|
// invalid key
|
||||||
try testing.expectError(Error.InvalidFormat, parseSingle("foo=ignore"));
|
try testing.expectError(Error.InvalidFormat, parseSingle("foo=ignore"));
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user