mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +03:00
input: parse global keys, document them
This commit is contained in:
@ -651,7 +651,8 @@ class: ?[:0]const u8 = null,
|
||||
@"working-directory": ?[]const u8 = null,
|
||||
|
||||
/// Key bindings. The format is `trigger=action`. Duplicate triggers will
|
||||
/// overwrite previously set values.
|
||||
/// overwrite previously set values. The list of actions is available in
|
||||
/// the documentation or using the `ghostty +list-actions` command.
|
||||
///
|
||||
/// Trigger: `+`-separated list of keys and modifiers. Example: `ctrl+a`,
|
||||
/// `ctrl+shift+b`, `up`. Some notes:
|
||||
@ -722,6 +723,9 @@ class: ?[:0]const u8 = null,
|
||||
/// * `text:text` - Send a string. Uses Zig string literal syntax.
|
||||
/// i.e. `text:\x15` sends Ctrl-U.
|
||||
///
|
||||
/// * All other actions can be found in the documentation or by using the
|
||||
/// `ghostty +list-actions` command.
|
||||
///
|
||||
/// Some notes for the action:
|
||||
///
|
||||
/// * The parameter is taken as-is after the `:`. Double quotes or
|
||||
@ -736,11 +740,38 @@ class: ?[:0]const u8 = null,
|
||||
/// removes ALL keybindings up to this point, including the default
|
||||
/// keybindings.
|
||||
///
|
||||
/// A keybind by default causes the input to be consumed. This means that the
|
||||
/// associated encoding (if any) will not be sent to the running program
|
||||
/// in the terminal. If you wish to send the encoded value to the program,
|
||||
/// specify the "unconsumed:" prefix before the entire keybind. For example:
|
||||
/// "unconsumed:ctrl+a=reload_config"
|
||||
/// The keybind trigger can be prefixed with some special values to change
|
||||
/// the behavior of the keybind. These are:
|
||||
///
|
||||
/// * `unconsumed:` - Do not consume the input. By default, a keybind
|
||||
/// will consume the input, meaning that the associated encoding (if
|
||||
/// any) will not be sent to the running program in the terminal. If
|
||||
/// you wish to send the encoded value to the program, specify the
|
||||
/// `unconsumed:` prefix before the entire keybind. For example:
|
||||
/// `unconsumed:ctrl+a=reload_config`
|
||||
///
|
||||
/// * `global:` - Make the keybind global. By default, keybinds only work
|
||||
/// within Ghostty and under the right conditions (application focused,
|
||||
/// sometimes terminal focused, etc.). If you want a keybind to work
|
||||
/// globally across your system (i.e. even when Ghostty is not focused),
|
||||
/// specify this prefix. Note: this does not work in all environments;
|
||||
/// see the additional notes below for more information.
|
||||
///
|
||||
/// Multiple prefixes can be specified. For example,
|
||||
/// `global:unconsumed:ctrl+a=reload_config` will make the keybind global
|
||||
/// and not consume the input to reload the config.
|
||||
///
|
||||
/// A note on `global:`: this feature is only supported on macOS. On macOS,
|
||||
/// this feature requires accessibility permissions to be granted to Ghostty.
|
||||
/// When a `global:` keybind is specified and Ghostty is launched or reloaded,
|
||||
/// Ghostty will attempt to request these permissions. If the permissions are
|
||||
/// not granted, the keybind will not work. On macOS, you can find these
|
||||
/// permissions in System Preferences -> Privacy & Security -> Accessibility.
|
||||
///
|
||||
/// Additionally, `global:` keybinds associated with actions that affect
|
||||
/// a specific terminal surface will target the last focused terminal surface
|
||||
/// within Ghostty. There is not a way to target a specific terminal surface
|
||||
/// with a `global:` keybind.
|
||||
keybind: Keybinds = .{},
|
||||
|
||||
/// Horizontal window padding. This applies padding between the terminal cells
|
||||
|
@ -17,17 +17,34 @@ action: Action,
|
||||
/// action is triggered.
|
||||
consumed: bool = true,
|
||||
|
||||
/// True if this binding is global. Global bindings should work system-wide
|
||||
/// 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,
|
||||
|
||||
pub const Error = error{
|
||||
InvalidFormat,
|
||||
InvalidAction,
|
||||
};
|
||||
|
||||
/// Flags the full binding-scoped flags that can be set per binding.
|
||||
pub const Flags = packed struct {
|
||||
/// True if this binding should consume the input when the
|
||||
/// action is triggered.
|
||||
consumed: bool = true,
|
||||
|
||||
/// True if this binding is global. Global bindings should work system-wide
|
||||
/// 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,
|
||||
};
|
||||
|
||||
/// Full binding parser. The binding parser is implemented as an iterator
|
||||
/// which yields elements to support multi-key sequences without allocation.
|
||||
pub const Parser = struct {
|
||||
unconsumed: bool = false,
|
||||
trigger_it: SequenceIterator,
|
||||
action: Action,
|
||||
flags: Flags = .{},
|
||||
|
||||
pub const Elem = union(enum) {
|
||||
/// A leader trigger in a sequence.
|
||||
@ -38,11 +55,7 @@ pub const Parser = struct {
|
||||
};
|
||||
|
||||
pub fn init(raw_input: []const u8) Error!Parser {
|
||||
// If our entire input is prefixed with "unconsumed:" then we are
|
||||
// not consuming this keybind when the action is triggered.
|
||||
const unconsumed_prefix = "unconsumed:";
|
||||
const unconsumed = std.mem.startsWith(u8, raw_input, unconsumed_prefix);
|
||||
const start_idx = if (unconsumed) unconsumed_prefix.len else 0;
|
||||
const flags, const start_idx = try parseFlags(raw_input);
|
||||
const input = raw_input[start_idx..];
|
||||
|
||||
// Find the first = which splits are mapping into the trigger
|
||||
@ -52,12 +65,44 @@ pub const Parser = struct {
|
||||
// Sequence iterator goes up to the equal, action is after. We can
|
||||
// parse the action now.
|
||||
return .{
|
||||
.unconsumed = unconsumed,
|
||||
.trigger_it = .{ .input = input[0..eql_idx] },
|
||||
.action = try Action.parse(input[eql_idx + 1 ..]),
|
||||
.flags = flags,
|
||||
};
|
||||
}
|
||||
|
||||
fn parseFlags(raw_input: []const u8) Error!struct { Flags, usize } {
|
||||
var flags: Flags = .{};
|
||||
|
||||
var start_idx: usize = 0;
|
||||
var input: []const u8 = raw_input;
|
||||
while (true) {
|
||||
// Find the next prefix
|
||||
const idx = std.mem.indexOf(u8, input, ":") orelse break;
|
||||
const prefix = input[0..idx];
|
||||
|
||||
// If the prefix is one of our flags then set it.
|
||||
if (std.mem.eql(u8, prefix, "unconsumed")) {
|
||||
if (!flags.consumed) return Error.InvalidFormat;
|
||||
flags.consumed = false;
|
||||
} else if (std.mem.eql(u8, prefix, "global")) {
|
||||
if (flags.global) return Error.InvalidFormat;
|
||||
flags.global = true;
|
||||
} else {
|
||||
// If we don't recognize the prefix then we're done.
|
||||
// There are trigger-specific prefixes like "physical:" so
|
||||
// this lets us fall into that.
|
||||
break;
|
||||
}
|
||||
|
||||
// Move past the prefix
|
||||
start_idx += idx + 1;
|
||||
input = input[idx + 1 ..];
|
||||
}
|
||||
|
||||
return .{ flags, start_idx };
|
||||
}
|
||||
|
||||
pub fn next(self: *Parser) Error!?Elem {
|
||||
// Get our trigger. If we're out of triggers then we're done.
|
||||
const trigger = (try self.trigger_it.next()) orelse return null;
|
||||
@ -69,7 +114,8 @@ pub const Parser = struct {
|
||||
return .{ .binding = .{
|
||||
.trigger = trigger,
|
||||
.action = self.action,
|
||||
.consumed = !self.unconsumed,
|
||||
.consumed = self.flags.consumed,
|
||||
.global = self.flags.global,
|
||||
} };
|
||||
}
|
||||
|
||||
@ -1241,6 +1287,41 @@ test "parse: triggers" {
|
||||
try testing.expectError(Error.InvalidFormat, parseSingle("a+b=ignore"));
|
||||
}
|
||||
|
||||
test "parse: global triggers" {
|
||||
const testing = std.testing;
|
||||
|
||||
// global keys
|
||||
try testing.expectEqual(Binding{
|
||||
.trigger = .{
|
||||
.mods = .{ .shift = true },
|
||||
.key = .{ .translated = .a },
|
||||
},
|
||||
.action = .{ .ignore = {} },
|
||||
.global = true,
|
||||
}, try parseSingle("global:shift+a=ignore"));
|
||||
|
||||
// global physical keys
|
||||
try testing.expectEqual(Binding{
|
||||
.trigger = .{
|
||||
.mods = .{ .shift = true },
|
||||
.key = .{ .physical = .a },
|
||||
},
|
||||
.action = .{ .ignore = {} },
|
||||
.global = true,
|
||||
}, try parseSingle("global:physical:a+shift=ignore"));
|
||||
|
||||
// global unconsumed keys
|
||||
try testing.expectEqual(Binding{
|
||||
.trigger = .{
|
||||
.mods = .{ .shift = true },
|
||||
.key = .{ .translated = .a },
|
||||
},
|
||||
.action = .{ .ignore = {} },
|
||||
.consumed = false,
|
||||
.global = true,
|
||||
}, try parseSingle("unconsumed:global:a+shift=ignore"));
|
||||
}
|
||||
|
||||
test "parse: modifier aliases" {
|
||||
const testing = std.testing;
|
||||
|
||||
|
Reference in New Issue
Block a user