mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
Allow keybinding arbitrary unicode codepoints (#1814)
Fixes #1802 This allows `keybind` configurations to map to any Unicode codepoint. This enables keybindings for which we don't have a registered keycode or for custom keyboard firmwares that may produce arbitrary text (but the Ghostty support is limited to a single codepoint). The `keybind` syntax is unchanged. If a bound character doesn't map to a known logical key that Ghostty knows about, we map it to a Unicode codepoint. The unicode codepoint is compared against the _unshifted codepoint_ from the apprt key event. Note that this binding is to a single _codepoint_. We don't support arbitrary sequences of characters or multi-code point graphemes for keybindings due to the complexity in memory management that would introduce. This also provides a good fallback for scenarios where it might make sense to educate Ghostty about a key code or fix a bug in our keyboard input system, but the unicode data is correct. In that scenario, unicode key binds should allow key binds to still work while we investigate the input issues. Example: ``` shift+ö=text:hello ``` This now works as expected on a US hardware keyboard with the Hungarian keyboard layout.
This commit is contained in:

committed by
GitHub

parent
f63927a7c4
commit
14417d2592
@ -330,10 +330,22 @@ typedef struct {
|
|||||||
bool composing;
|
bool composing;
|
||||||
} ghostty_input_key_s;
|
} ghostty_input_key_s;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
GHOSTTY_TRIGGER_TRANSLATED,
|
||||||
|
GHOSTTY_TRIGGER_PHYSICAL,
|
||||||
|
GHOSTTY_TRIGGER_UNICODE,
|
||||||
|
} ghostty_input_trigger_tag_e;
|
||||||
|
|
||||||
|
typedef union {
|
||||||
|
ghostty_input_key_e translated;
|
||||||
|
ghostty_input_key_e physical;
|
||||||
|
uint32_t unicode;
|
||||||
|
} ghostty_input_trigger_key_u;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
ghostty_input_key_e key;
|
ghostty_input_trigger_tag_e tag;
|
||||||
|
ghostty_input_trigger_key_u key;
|
||||||
ghostty_input_mods_e mods;
|
ghostty_input_mods_e mods;
|
||||||
bool physical;
|
|
||||||
} ghostty_input_trigger_s;
|
} ghostty_input_trigger_s;
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
|
@ -115,7 +115,28 @@ 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))
|
||||||
guard let equiv = Ghostty.keyEquivalent(key: trigger.key) else { return nil }
|
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(
|
return KeyEquivalent(
|
||||||
key: equiv,
|
key: equiv,
|
||||||
|
@ -1188,7 +1188,7 @@ pub fn keyCallback(
|
|||||||
const binding_mods = event.mods.binding();
|
const binding_mods = event.mods.binding();
|
||||||
var trigger: input.Binding.Trigger = .{
|
var trigger: input.Binding.Trigger = .{
|
||||||
.mods = binding_mods,
|
.mods = binding_mods,
|
||||||
.key = event.key,
|
.key = .{ .translated = event.key },
|
||||||
};
|
};
|
||||||
|
|
||||||
const set = self.config.keybind.set;
|
const set = self.config.keybind.set;
|
||||||
@ -1198,14 +1198,22 @@ pub fn keyCallback(
|
|||||||
set.getConsumed(trigger),
|
set.getConsumed(trigger),
|
||||||
};
|
};
|
||||||
|
|
||||||
trigger.key = event.physical_key;
|
trigger.key = .{ .physical = event.physical_key };
|
||||||
trigger.physical = true;
|
|
||||||
if (set.get(trigger)) |v| break :action .{
|
if (set.get(trigger)) |v| break :action .{
|
||||||
v,
|
v,
|
||||||
trigger,
|
trigger,
|
||||||
set.getConsumed(trigger),
|
set.getConsumed(trigger),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (event.unshifted_codepoint > 0) {
|
||||||
|
trigger.key = .{ .unicode = event.unshifted_codepoint };
|
||||||
|
if (set.get(trigger)) |v| break :action .{
|
||||||
|
v,
|
||||||
|
trigger,
|
||||||
|
set.getConsumed(trigger),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
break :binding;
|
break :binding;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -14,8 +14,14 @@ pub fn accelFromTrigger(buf: []u8, trigger: input.Binding.Trigger) !?[:0]const u
|
|||||||
if (trigger.mods.super) try writer.writeAll("<Super>");
|
if (trigger.mods.super) try writer.writeAll("<Super>");
|
||||||
|
|
||||||
// Write our key
|
// Write our key
|
||||||
const keyval = keyvalFromKey(trigger.key) orelse return null;
|
switch (trigger.key) {
|
||||||
try writer.writeAll(std.mem.sliceTo(c.gdk_keyval_name(keyval), 0));
|
.physical, .translated => |k| {
|
||||||
|
const keyval = keyvalFromKey(k) orelse return null;
|
||||||
|
try writer.writeAll(std.mem.sliceTo(c.gdk_keyval_name(keyval), 0));
|
||||||
|
},
|
||||||
|
|
||||||
|
.unicode => |cp| try writer.print("{u}", .{cp}),
|
||||||
|
}
|
||||||
|
|
||||||
// We need to make the string null terminated.
|
// We need to make the string null terminated.
|
||||||
try writer.writeByte(0);
|
try writer.writeByte(0);
|
||||||
|
@ -96,7 +96,7 @@ export fn ghostty_config_trigger(
|
|||||||
self: *Config,
|
self: *Config,
|
||||||
str: [*]const u8,
|
str: [*]const u8,
|
||||||
len: usize,
|
len: usize,
|
||||||
) inputpkg.Binding.Trigger {
|
) inputpkg.Binding.Trigger.C {
|
||||||
return config_trigger_(self, str[0..len]) catch |err| err: {
|
return config_trigger_(self, str[0..len]) catch |err| err: {
|
||||||
log.err("error finding trigger err={}", .{err});
|
log.err("error finding trigger err={}", .{err});
|
||||||
break :err .{};
|
break :err .{};
|
||||||
@ -106,9 +106,10 @@ export fn ghostty_config_trigger(
|
|||||||
fn config_trigger_(
|
fn config_trigger_(
|
||||||
self: *Config,
|
self: *Config,
|
||||||
str: []const u8,
|
str: []const u8,
|
||||||
) !inputpkg.Binding.Trigger {
|
) !inputpkg.Binding.Trigger.C {
|
||||||
const action = try inputpkg.Binding.Action.parse(str);
|
const action = try inputpkg.Binding.Action.parse(str);
|
||||||
return self.keybind.set.getTrigger(action) orelse .{};
|
const trigger: inputpkg.Binding.Trigger = self.keybind.set.getTrigger(action) orelse .{};
|
||||||
|
return trigger.cval();
|
||||||
}
|
}
|
||||||
|
|
||||||
export fn ghostty_config_errors_count(self: *Config) u32 {
|
export fn ghostty_config_errors_count(self: *Config) u32 {
|
||||||
|
@ -1100,12 +1100,12 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
|
|||||||
// keybinds for opening and reloading config
|
// keybinds for opening and reloading config
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .comma, .mods = inputpkg.ctrlOrSuper(.{ .shift = true }) },
|
.{ .key = .{ .translated = .comma }, .mods = inputpkg.ctrlOrSuper(.{ .shift = true }) },
|
||||||
.{ .reload_config = {} },
|
.{ .reload_config = {} },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .comma, .mods = inputpkg.ctrlOrSuper(.{}) },
|
.{ .key = .{ .translated = .comma }, .mods = inputpkg.ctrlOrSuper(.{}) },
|
||||||
.{ .open_config = {} },
|
.{ .open_config = {} },
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -1119,12 +1119,12 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
|
|||||||
|
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .c, .mods = mods },
|
.{ .key = .{ .translated = .c }, .mods = mods },
|
||||||
.{ .copy_to_clipboard = {} },
|
.{ .copy_to_clipboard = {} },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .v, .mods = mods },
|
.{ .key = .{ .translated = .v }, .mods = mods },
|
||||||
.{ .paste_from_clipboard = {} },
|
.{ .paste_from_clipboard = {} },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1132,29 +1132,29 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
|
|||||||
// Fonts
|
// Fonts
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .equal, .mods = inputpkg.ctrlOrSuper(.{}) },
|
.{ .key = .{ .translated = .equal }, .mods = inputpkg.ctrlOrSuper(.{}) },
|
||||||
.{ .increase_font_size = 1 },
|
.{ .increase_font_size = 1 },
|
||||||
);
|
);
|
||||||
// Increase font size mapping for keyboards with dedicated plus keys (like german)
|
// Increase font size mapping for keyboards with dedicated plus keys (like german)
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .plus, .mods = inputpkg.ctrlOrSuper(.{}) },
|
.{ .key = .{ .translated = .plus }, .mods = inputpkg.ctrlOrSuper(.{}) },
|
||||||
.{ .increase_font_size = 1 },
|
.{ .increase_font_size = 1 },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .minus, .mods = inputpkg.ctrlOrSuper(.{}) },
|
.{ .key = .{ .translated = .minus }, .mods = inputpkg.ctrlOrSuper(.{}) },
|
||||||
.{ .decrease_font_size = 1 },
|
.{ .decrease_font_size = 1 },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .zero, .mods = inputpkg.ctrlOrSuper(.{}) },
|
.{ .key = .{ .translated = .zero }, .mods = inputpkg.ctrlOrSuper(.{}) },
|
||||||
.{ .reset_font_size = {} },
|
.{ .reset_font_size = {} },
|
||||||
);
|
);
|
||||||
|
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .j, .mods = inputpkg.ctrlOrSuper(.{ .shift = true }) },
|
.{ .key = .{ .translated = .j }, .mods = inputpkg.ctrlOrSuper(.{ .shift = true }) },
|
||||||
.{ .write_scrollback_file = {} },
|
.{ .write_scrollback_file = {} },
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -1162,169 +1162,169 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
|
|||||||
if (comptime !builtin.target.isDarwin()) {
|
if (comptime !builtin.target.isDarwin()) {
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .n, .mods = .{ .ctrl = true, .shift = true } },
|
.{ .key = .{ .translated = .n }, .mods = .{ .ctrl = true, .shift = true } },
|
||||||
.{ .new_window = {} },
|
.{ .new_window = {} },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .w, .mods = .{ .ctrl = true, .shift = true } },
|
.{ .key = .{ .translated = .w }, .mods = .{ .ctrl = true, .shift = true } },
|
||||||
.{ .close_surface = {} },
|
.{ .close_surface = {} },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .q, .mods = .{ .ctrl = true, .shift = true } },
|
.{ .key = .{ .translated = .q }, .mods = .{ .ctrl = true, .shift = true } },
|
||||||
.{ .quit = {} },
|
.{ .quit = {} },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .f4, .mods = .{ .alt = true } },
|
.{ .key = .{ .translated = .f4 }, .mods = .{ .alt = true } },
|
||||||
.{ .close_window = {} },
|
.{ .close_window = {} },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .t, .mods = .{ .ctrl = true, .shift = true } },
|
.{ .key = .{ .translated = .t }, .mods = .{ .ctrl = true, .shift = true } },
|
||||||
.{ .new_tab = {} },
|
.{ .new_tab = {} },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .left, .mods = .{ .ctrl = true, .shift = true } },
|
.{ .key = .{ .translated = .left }, .mods = .{ .ctrl = true, .shift = true } },
|
||||||
.{ .previous_tab = {} },
|
.{ .previous_tab = {} },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .right, .mods = .{ .ctrl = true, .shift = true } },
|
.{ .key = .{ .translated = .right }, .mods = .{ .ctrl = true, .shift = true } },
|
||||||
.{ .next_tab = {} },
|
.{ .next_tab = {} },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .page_up, .mods = .{ .ctrl = true } },
|
.{ .key = .{ .translated = .page_up }, .mods = .{ .ctrl = true } },
|
||||||
.{ .previous_tab = {} },
|
.{ .previous_tab = {} },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .page_down, .mods = .{ .ctrl = true } },
|
.{ .key = .{ .translated = .page_down }, .mods = .{ .ctrl = true } },
|
||||||
.{ .next_tab = {} },
|
.{ .next_tab = {} },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .o, .mods = .{ .ctrl = true, .shift = true } },
|
.{ .key = .{ .translated = .o }, .mods = .{ .ctrl = true, .shift = true } },
|
||||||
.{ .new_split = .right },
|
.{ .new_split = .right },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .e, .mods = .{ .ctrl = true, .shift = true } },
|
.{ .key = .{ .translated = .e }, .mods = .{ .ctrl = true, .shift = true } },
|
||||||
.{ .new_split = .down },
|
.{ .new_split = .down },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .left_bracket, .mods = .{ .ctrl = true, .super = true } },
|
.{ .key = .{ .translated = .left_bracket }, .mods = .{ .ctrl = true, .super = true } },
|
||||||
.{ .goto_split = .previous },
|
.{ .goto_split = .previous },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .right_bracket, .mods = .{ .ctrl = true, .super = true } },
|
.{ .key = .{ .translated = .right_bracket }, .mods = .{ .ctrl = true, .super = true } },
|
||||||
.{ .goto_split = .next },
|
.{ .goto_split = .next },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .up, .mods = .{ .ctrl = true, .alt = true } },
|
.{ .key = .{ .translated = .up }, .mods = .{ .ctrl = true, .alt = true } },
|
||||||
.{ .goto_split = .top },
|
.{ .goto_split = .top },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .down, .mods = .{ .ctrl = true, .alt = true } },
|
.{ .key = .{ .translated = .down }, .mods = .{ .ctrl = true, .alt = true } },
|
||||||
.{ .goto_split = .bottom },
|
.{ .goto_split = .bottom },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .left, .mods = .{ .ctrl = true, .alt = true } },
|
.{ .key = .{ .translated = .left }, .mods = .{ .ctrl = true, .alt = true } },
|
||||||
.{ .goto_split = .left },
|
.{ .goto_split = .left },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .right, .mods = .{ .ctrl = true, .alt = true } },
|
.{ .key = .{ .translated = .right }, .mods = .{ .ctrl = true, .alt = true } },
|
||||||
.{ .goto_split = .right },
|
.{ .goto_split = .right },
|
||||||
);
|
);
|
||||||
|
|
||||||
// Resizing splits
|
// Resizing splits
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .up, .mods = .{ .super = true, .ctrl = true, .shift = true } },
|
.{ .key = .{ .translated = .up }, .mods = .{ .super = true, .ctrl = true, .shift = true } },
|
||||||
.{ .resize_split = .{ .up, 10 } },
|
.{ .resize_split = .{ .up, 10 } },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .down, .mods = .{ .super = true, .ctrl = true, .shift = true } },
|
.{ .key = .{ .translated = .down }, .mods = .{ .super = true, .ctrl = true, .shift = true } },
|
||||||
.{ .resize_split = .{ .down, 10 } },
|
.{ .resize_split = .{ .down, 10 } },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .left, .mods = .{ .super = true, .ctrl = true, .shift = true } },
|
.{ .key = .{ .translated = .left }, .mods = .{ .super = true, .ctrl = true, .shift = true } },
|
||||||
.{ .resize_split = .{ .left, 10 } },
|
.{ .resize_split = .{ .left, 10 } },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .right, .mods = .{ .super = true, .ctrl = true, .shift = true } },
|
.{ .key = .{ .translated = .right }, .mods = .{ .super = true, .ctrl = true, .shift = true } },
|
||||||
.{ .resize_split = .{ .right, 10 } },
|
.{ .resize_split = .{ .right, 10 } },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .equal, .mods = .{ .super = true, .ctrl = true, .shift = true } },
|
.{ .key = .{ .translated = .equal }, .mods = .{ .super = true, .ctrl = true, .shift = true } },
|
||||||
.{ .equalize_splits = {} },
|
.{ .equalize_splits = {} },
|
||||||
);
|
);
|
||||||
|
|
||||||
// Viewport scrolling
|
// Viewport scrolling
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .home, .mods = .{ .shift = true } },
|
.{ .key = .{ .translated = .home }, .mods = .{ .shift = true } },
|
||||||
.{ .scroll_to_top = {} },
|
.{ .scroll_to_top = {} },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .end, .mods = .{ .shift = true } },
|
.{ .key = .{ .translated = .end }, .mods = .{ .shift = true } },
|
||||||
.{ .scroll_to_bottom = {} },
|
.{ .scroll_to_bottom = {} },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .page_up, .mods = .{ .shift = true } },
|
.{ .key = .{ .translated = .page_up }, .mods = .{ .shift = true } },
|
||||||
.{ .scroll_page_up = {} },
|
.{ .scroll_page_up = {} },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .page_down, .mods = .{ .shift = true } },
|
.{ .key = .{ .translated = .page_down }, .mods = .{ .shift = true } },
|
||||||
.{ .scroll_page_down = {} },
|
.{ .scroll_page_down = {} },
|
||||||
);
|
);
|
||||||
|
|
||||||
// Semantic prompts
|
// Semantic prompts
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .page_up, .mods = .{ .shift = true, .ctrl = true } },
|
.{ .key = .{ .translated = .page_up }, .mods = .{ .shift = true, .ctrl = true } },
|
||||||
.{ .jump_to_prompt = -1 },
|
.{ .jump_to_prompt = -1 },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .page_down, .mods = .{ .shift = true, .ctrl = true } },
|
.{ .key = .{ .translated = .page_down }, .mods = .{ .shift = true, .ctrl = true } },
|
||||||
.{ .jump_to_prompt = 1 },
|
.{ .jump_to_prompt = 1 },
|
||||||
);
|
);
|
||||||
|
|
||||||
// Inspector, matching Chromium
|
// Inspector, matching Chromium
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .i, .mods = .{ .shift = true, .ctrl = true } },
|
.{ .key = .{ .translated = .i }, .mods = .{ .shift = true, .ctrl = true } },
|
||||||
.{ .inspector = .toggle },
|
.{ .inspector = .toggle },
|
||||||
);
|
);
|
||||||
|
|
||||||
// Terminal
|
// Terminal
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .a, .mods = .{ .shift = true, .ctrl = true } },
|
.{ .key = .{ .translated = .a }, .mods = .{ .shift = true, .ctrl = true } },
|
||||||
.{ .select_all = {} },
|
.{ .select_all = {} },
|
||||||
);
|
);
|
||||||
|
|
||||||
// Selection clipboard paste
|
// Selection clipboard paste
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .insert, .mods = .{ .shift = true } },
|
.{ .key = .{ .translated = .insert }, .mods = .{ .shift = true } },
|
||||||
.{ .paste_from_selection = {} },
|
.{ .paste_from_selection = {} },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1344,15 +1344,17 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
|
|||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{
|
.{
|
||||||
.key = @enumFromInt(i),
|
|
||||||
.mods = mods,
|
|
||||||
|
|
||||||
// On macOS, we use the physical key for tab changing so
|
// On macOS, we use the physical key for tab changing so
|
||||||
// that this works across all keyboard layouts. This may
|
// that this works across all keyboard layouts. This may
|
||||||
// want to be true on other platforms as well but this
|
// want to be true on other platforms as well but this
|
||||||
// is definitely true on macOS so we just do it here for
|
// is definitely true on macOS so we just do it here for
|
||||||
// now (#817)
|
// now (#817)
|
||||||
.physical = builtin.target.isDarwin(),
|
.key = if (comptime builtin.target.isDarwin())
|
||||||
|
.{ .physical = @enumFromInt(i) }
|
||||||
|
else
|
||||||
|
.{ .translated = @enumFromInt(i) },
|
||||||
|
|
||||||
|
.mods = mods,
|
||||||
},
|
},
|
||||||
.{ .goto_tab = (i - start) + 1 },
|
.{ .goto_tab = (i - start) + 1 },
|
||||||
);
|
);
|
||||||
@ -1362,14 +1364,14 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
|
|||||||
// Toggle fullscreen
|
// Toggle fullscreen
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .enter, .mods = inputpkg.ctrlOrSuper(.{}) },
|
.{ .key = .{ .translated = .enter }, .mods = inputpkg.ctrlOrSuper(.{}) },
|
||||||
.{ .toggle_fullscreen = {} },
|
.{ .toggle_fullscreen = {} },
|
||||||
);
|
);
|
||||||
|
|
||||||
// Toggle zoom a split
|
// Toggle zoom a split
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .enter, .mods = inputpkg.ctrlOrSuper(.{ .shift = true }) },
|
.{ .key = .{ .translated = .enter }, .mods = inputpkg.ctrlOrSuper(.{ .shift = true }) },
|
||||||
.{ .toggle_split_zoom = {} },
|
.{ .toggle_split_zoom = {} },
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -1377,167 +1379,167 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
|
|||||||
if (comptime builtin.target.isDarwin()) {
|
if (comptime builtin.target.isDarwin()) {
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .q, .mods = .{ .super = true } },
|
.{ .key = .{ .translated = .q }, .mods = .{ .super = true } },
|
||||||
.{ .quit = {} },
|
.{ .quit = {} },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .k, .mods = .{ .super = true } },
|
.{ .key = .{ .translated = .k }, .mods = .{ .super = true } },
|
||||||
.{ .clear_screen = {} },
|
.{ .clear_screen = {} },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .a, .mods = .{ .super = true } },
|
.{ .key = .{ .translated = .a }, .mods = .{ .super = true } },
|
||||||
.{ .select_all = {} },
|
.{ .select_all = {} },
|
||||||
);
|
);
|
||||||
|
|
||||||
// Viewport scrolling
|
// Viewport scrolling
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .home, .mods = .{ .super = true } },
|
.{ .key = .{ .translated = .home }, .mods = .{ .super = true } },
|
||||||
.{ .scroll_to_top = {} },
|
.{ .scroll_to_top = {} },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .end, .mods = .{ .super = true } },
|
.{ .key = .{ .translated = .end }, .mods = .{ .super = true } },
|
||||||
.{ .scroll_to_bottom = {} },
|
.{ .scroll_to_bottom = {} },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .page_up, .mods = .{ .super = true } },
|
.{ .key = .{ .translated = .page_up }, .mods = .{ .super = true } },
|
||||||
.{ .scroll_page_up = {} },
|
.{ .scroll_page_up = {} },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .page_down, .mods = .{ .super = true } },
|
.{ .key = .{ .translated = .page_down }, .mods = .{ .super = true } },
|
||||||
.{ .scroll_page_down = {} },
|
.{ .scroll_page_down = {} },
|
||||||
);
|
);
|
||||||
|
|
||||||
// Semantic prompts
|
// Semantic prompts
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .up, .mods = .{ .super = true, .shift = true } },
|
.{ .key = .{ .translated = .up }, .mods = .{ .super = true, .shift = true } },
|
||||||
.{ .jump_to_prompt = -1 },
|
.{ .jump_to_prompt = -1 },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .down, .mods = .{ .super = true, .shift = true } },
|
.{ .key = .{ .translated = .down }, .mods = .{ .super = true, .shift = true } },
|
||||||
.{ .jump_to_prompt = 1 },
|
.{ .jump_to_prompt = 1 },
|
||||||
);
|
);
|
||||||
|
|
||||||
// Mac windowing
|
// Mac windowing
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .n, .mods = .{ .super = true } },
|
.{ .key = .{ .translated = .n }, .mods = .{ .super = true } },
|
||||||
.{ .new_window = {} },
|
.{ .new_window = {} },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .w, .mods = .{ .super = true } },
|
.{ .key = .{ .translated = .w }, .mods = .{ .super = true } },
|
||||||
.{ .close_surface = {} },
|
.{ .close_surface = {} },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .w, .mods = .{ .super = true, .shift = true } },
|
.{ .key = .{ .translated = .w }, .mods = .{ .super = true, .shift = true } },
|
||||||
.{ .close_window = {} },
|
.{ .close_window = {} },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .w, .mods = .{ .super = true, .shift = true, .alt = true } },
|
.{ .key = .{ .translated = .w }, .mods = .{ .super = true, .shift = true, .alt = true } },
|
||||||
.{ .close_all_windows = {} },
|
.{ .close_all_windows = {} },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .t, .mods = .{ .super = true } },
|
.{ .key = .{ .translated = .t }, .mods = .{ .super = true } },
|
||||||
.{ .new_tab = {} },
|
.{ .new_tab = {} },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .left_bracket, .mods = .{ .super = true, .shift = true } },
|
.{ .key = .{ .translated = .left_bracket }, .mods = .{ .super = true, .shift = true } },
|
||||||
.{ .previous_tab = {} },
|
.{ .previous_tab = {} },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .right_bracket, .mods = .{ .super = true, .shift = true } },
|
.{ .key = .{ .translated = .right_bracket }, .mods = .{ .super = true, .shift = true } },
|
||||||
.{ .next_tab = {} },
|
.{ .next_tab = {} },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .d, .mods = .{ .super = true } },
|
.{ .key = .{ .translated = .d }, .mods = .{ .super = true } },
|
||||||
.{ .new_split = .right },
|
.{ .new_split = .right },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .d, .mods = .{ .super = true, .shift = true } },
|
.{ .key = .{ .translated = .d }, .mods = .{ .super = true, .shift = true } },
|
||||||
.{ .new_split = .down },
|
.{ .new_split = .down },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .left_bracket, .mods = .{ .super = true } },
|
.{ .key = .{ .translated = .left_bracket }, .mods = .{ .super = true } },
|
||||||
.{ .goto_split = .previous },
|
.{ .goto_split = .previous },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .right_bracket, .mods = .{ .super = true } },
|
.{ .key = .{ .translated = .right_bracket }, .mods = .{ .super = true } },
|
||||||
.{ .goto_split = .next },
|
.{ .goto_split = .next },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .up, .mods = .{ .super = true, .alt = true } },
|
.{ .key = .{ .translated = .up }, .mods = .{ .super = true, .alt = true } },
|
||||||
.{ .goto_split = .top },
|
.{ .goto_split = .top },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .down, .mods = .{ .super = true, .alt = true } },
|
.{ .key = .{ .translated = .down }, .mods = .{ .super = true, .alt = true } },
|
||||||
.{ .goto_split = .bottom },
|
.{ .goto_split = .bottom },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .left, .mods = .{ .super = true, .alt = true } },
|
.{ .key = .{ .translated = .left }, .mods = .{ .super = true, .alt = true } },
|
||||||
.{ .goto_split = .left },
|
.{ .goto_split = .left },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .right, .mods = .{ .super = true, .alt = true } },
|
.{ .key = .{ .translated = .right }, .mods = .{ .super = true, .alt = true } },
|
||||||
.{ .goto_split = .right },
|
.{ .goto_split = .right },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .up, .mods = .{ .super = true, .ctrl = true } },
|
.{ .key = .{ .translated = .up }, .mods = .{ .super = true, .ctrl = true } },
|
||||||
.{ .resize_split = .{ .up, 10 } },
|
.{ .resize_split = .{ .up, 10 } },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .down, .mods = .{ .super = true, .ctrl = true } },
|
.{ .key = .{ .translated = .down }, .mods = .{ .super = true, .ctrl = true } },
|
||||||
.{ .resize_split = .{ .down, 10 } },
|
.{ .resize_split = .{ .down, 10 } },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .left, .mods = .{ .super = true, .ctrl = true } },
|
.{ .key = .{ .translated = .left }, .mods = .{ .super = true, .ctrl = true } },
|
||||||
.{ .resize_split = .{ .left, 10 } },
|
.{ .resize_split = .{ .left, 10 } },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .right, .mods = .{ .super = true, .ctrl = true } },
|
.{ .key = .{ .translated = .right }, .mods = .{ .super = true, .ctrl = true } },
|
||||||
.{ .resize_split = .{ .right, 10 } },
|
.{ .resize_split = .{ .right, 10 } },
|
||||||
);
|
);
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .equal, .mods = .{ .shift = true, .alt = true } },
|
.{ .key = .{ .translated = .equal }, .mods = .{ .shift = true, .alt = true } },
|
||||||
.{ .equalize_splits = {} },
|
.{ .equalize_splits = {} },
|
||||||
);
|
);
|
||||||
|
|
||||||
// Inspector, matching Chromium
|
// Inspector, matching Chromium
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .i, .mods = .{ .alt = true, .super = true } },
|
.{ .key = .{ .translated = .i }, .mods = .{ .alt = true, .super = true } },
|
||||||
.{ .inspector = .toggle },
|
.{ .inspector = .toggle },
|
||||||
);
|
);
|
||||||
|
|
||||||
// Alternate keybind, common to Mac programs
|
// Alternate keybind, common to Mac programs
|
||||||
try result.keybind.set.put(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .f, .mods = .{ .super = true, .ctrl = true } },
|
.{ .key = .{ .translated = .f }, .mods = .{ .super = true, .ctrl = true } },
|
||||||
.{ .toggle_fullscreen = {} },
|
.{ .toggle_fullscreen = {} },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -79,11 +79,9 @@ pub fn parse(raw_input: []const u8) !Binding {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If the key starts with "physical" then this is an physical key.
|
// If the key starts with "physical" then this is an physical key.
|
||||||
const physical = "physical:";
|
const physical_prefix = "physical:";
|
||||||
const key_part = if (std.mem.startsWith(u8, part, physical)) key_part: {
|
const physical = std.mem.startsWith(u8, part, physical_prefix);
|
||||||
result.physical = true;
|
const key_part = if (physical) part[physical_prefix.len..] else part;
|
||||||
break :key_part part[physical.len..];
|
|
||||||
} else part;
|
|
||||||
|
|
||||||
// Check if its a key
|
// Check if its a key
|
||||||
const keysInfo = @typeInfo(key.Key).Enum;
|
const keysInfo = @typeInfo(key.Key).Enum;
|
||||||
@ -91,14 +89,33 @@ pub fn parse(raw_input: []const u8) !Binding {
|
|||||||
if (!std.mem.eql(u8, field.name, "invalid")) {
|
if (!std.mem.eql(u8, field.name, "invalid")) {
|
||||||
if (std.mem.eql(u8, key_part, field.name)) {
|
if (std.mem.eql(u8, key_part, field.name)) {
|
||||||
// Repeat not allowed
|
// Repeat not allowed
|
||||||
if (result.key != .invalid) return Error.InvalidFormat;
|
if (!result.isKeyUnset()) return Error.InvalidFormat;
|
||||||
|
|
||||||
result.key = @field(key.Key, field.name);
|
const keyval = @field(key.Key, field.name);
|
||||||
|
result.key = if (physical)
|
||||||
|
.{ .physical = keyval }
|
||||||
|
else
|
||||||
|
.{ .translated = keyval };
|
||||||
continue :loop;
|
continue :loop;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we're still unset and we have exactly one unicode
|
||||||
|
// character then we can use that as a key.
|
||||||
|
if (result.isKeyUnset()) unicode: {
|
||||||
|
// Invalid UTF8 drops to invalid format
|
||||||
|
const view = std.unicode.Utf8View.init(key_part) catch break :unicode;
|
||||||
|
var it = view.iterator();
|
||||||
|
|
||||||
|
// No codepoints or multiple codepoints drops to invalid format
|
||||||
|
const cp = it.nextCodepoint() orelse break :unicode;
|
||||||
|
if (it.nextCodepoint() != null) break :unicode;
|
||||||
|
|
||||||
|
result.key = .{ .unicode = cp };
|
||||||
|
continue :loop;
|
||||||
|
}
|
||||||
|
|
||||||
// We didn't recognize this value
|
// We didn't recognize this value
|
||||||
return Error.InvalidFormat;
|
return Error.InvalidFormat;
|
||||||
}
|
}
|
||||||
@ -499,28 +516,80 @@ pub const Key = enum(c_int) {
|
|||||||
/// This is an extern struct because this is also used in the C API.
|
/// This is an extern struct because this is also used in the C API.
|
||||||
///
|
///
|
||||||
/// This must be kept in sync with include/ghostty.h ghostty_input_trigger_s
|
/// This must be kept in sync with include/ghostty.h ghostty_input_trigger_s
|
||||||
pub const Trigger = extern struct {
|
pub const Trigger = struct {
|
||||||
/// The key that has to be pressed for a binding to take action.
|
/// The key that has to be pressed for a binding to take action.
|
||||||
key: key.Key = .invalid,
|
key: Trigger.Key = .{ .translated = .invalid },
|
||||||
|
|
||||||
/// The key modifiers that must be active for this to match.
|
/// The key modifiers that must be active for this to match.
|
||||||
mods: key.Mods = .{},
|
mods: key.Mods = .{},
|
||||||
|
|
||||||
/// key is the "physical" version. This is the same as mapped for
|
pub const Key = union(C.Tag) {
|
||||||
/// standard US keyboard layouts. For non-US keyboard layouts, this
|
/// key is the translated version of a key. This is the key that
|
||||||
/// is used to bind to a physical key location rather than a translated
|
/// a logical keyboard layout at the OS level would translate the
|
||||||
/// key.
|
/// physical key to. For example if you use a US hardware keyboard
|
||||||
physical: bool = false,
|
/// but have a Dvorak layout, the key would be the Dvorak key.
|
||||||
|
translated: key.Key,
|
||||||
|
|
||||||
|
/// key is the "physical" version. This is the same as mapped for
|
||||||
|
/// standard US keyboard layouts. For non-US keyboard layouts, this
|
||||||
|
/// is used to bind to a physical key location rather than a translated
|
||||||
|
/// key.
|
||||||
|
physical: key.Key,
|
||||||
|
|
||||||
|
/// This is used for binding to keys that produce a certain unicode
|
||||||
|
/// codepoint. This is useful for binding to keys that don't have a
|
||||||
|
/// registered keycode with Ghostty.
|
||||||
|
unicode: u21,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The extern struct used for triggers in the C API.
|
||||||
|
pub const C = extern struct {
|
||||||
|
tag: Tag = .translated,
|
||||||
|
key: C.Key = .{ .translated = .invalid },
|
||||||
|
mods: key.Mods = .{},
|
||||||
|
|
||||||
|
pub const Tag = enum(c_int) {
|
||||||
|
translated,
|
||||||
|
physical,
|
||||||
|
unicode,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Key = extern union {
|
||||||
|
translated: key.Key,
|
||||||
|
physical: key.Key,
|
||||||
|
unicode: u32,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Returns true if this trigger has no key set.
|
||||||
|
pub fn isKeyUnset(self: Trigger) bool {
|
||||||
|
return switch (self.key) {
|
||||||
|
.translated => |v| v == .invalid,
|
||||||
|
else => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns a hash code that can be used to uniquely identify this trigger.
|
/// Returns a hash code that can be used to uniquely identify this trigger.
|
||||||
pub fn hash(self: Trigger) u64 {
|
pub fn hash(self: Trigger) u64 {
|
||||||
var hasher = std.hash.Wyhash.init(0);
|
var hasher = std.hash.Wyhash.init(0);
|
||||||
std.hash.autoHash(&hasher, self.key);
|
std.hash.autoHash(&hasher, self.key);
|
||||||
std.hash.autoHash(&hasher, self.mods.binding());
|
std.hash.autoHash(&hasher, self.mods.binding());
|
||||||
std.hash.autoHash(&hasher, self.physical);
|
|
||||||
return hasher.final();
|
return hasher.final();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convert the trigger to a C API compatible trigger.
|
||||||
|
pub fn cval(self: Trigger) C {
|
||||||
|
return .{
|
||||||
|
.tag = self.key,
|
||||||
|
.key = switch (self.key) {
|
||||||
|
.translated => |v| .{ .translated = v },
|
||||||
|
.physical => |v| .{ .physical = v },
|
||||||
|
.unicode => |v| .{ .unicode = @intCast(v) },
|
||||||
|
},
|
||||||
|
.mods = self.mods,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// Format implementation for fmt package.
|
/// Format implementation for fmt package.
|
||||||
pub fn format(
|
pub fn format(
|
||||||
self: Trigger,
|
self: Trigger,
|
||||||
@ -538,8 +607,11 @@ pub const Trigger = extern struct {
|
|||||||
if (self.mods.shift) try writer.writeAll("shift+");
|
if (self.mods.shift) try writer.writeAll("shift+");
|
||||||
|
|
||||||
// Key
|
// Key
|
||||||
if (self.physical) try writer.writeAll("physical:");
|
switch (self.key) {
|
||||||
try writer.print("{s}", .{@tagName(self.key)});
|
.translated => |k| try writer.print("{s}", .{@tagName(k)}),
|
||||||
|
.physical => |k| try writer.print("physical:{s}", .{@tagName(k)}),
|
||||||
|
.unicode => |c| try writer.print("{u}", .{c}),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -720,7 +792,7 @@ test "parse: triggers" {
|
|||||||
// single character
|
// single character
|
||||||
try testing.expectEqual(
|
try testing.expectEqual(
|
||||||
Binding{
|
Binding{
|
||||||
.trigger = .{ .key = .a },
|
.trigger = .{ .key = .{ .translated = .a } },
|
||||||
.action = .{ .ignore = {} },
|
.action = .{ .ignore = {} },
|
||||||
},
|
},
|
||||||
try parse("a=ignore"),
|
try parse("a=ignore"),
|
||||||
@ -730,14 +802,14 @@ test "parse: triggers" {
|
|||||||
try testing.expectEqual(Binding{
|
try testing.expectEqual(Binding{
|
||||||
.trigger = .{
|
.trigger = .{
|
||||||
.mods = .{ .shift = true },
|
.mods = .{ .shift = true },
|
||||||
.key = .a,
|
.key = .{ .translated = .a },
|
||||||
},
|
},
|
||||||
.action = .{ .ignore = {} },
|
.action = .{ .ignore = {} },
|
||||||
}, try parse("shift+a=ignore"));
|
}, try parse("shift+a=ignore"));
|
||||||
try testing.expectEqual(Binding{
|
try testing.expectEqual(Binding{
|
||||||
.trigger = .{
|
.trigger = .{
|
||||||
.mods = .{ .ctrl = true },
|
.mods = .{ .ctrl = true },
|
||||||
.key = .a,
|
.key = .{ .translated = .a },
|
||||||
},
|
},
|
||||||
.action = .{ .ignore = {} },
|
.action = .{ .ignore = {} },
|
||||||
}, try parse("ctrl+a=ignore"));
|
}, try parse("ctrl+a=ignore"));
|
||||||
@ -746,7 +818,7 @@ test "parse: triggers" {
|
|||||||
try testing.expectEqual(Binding{
|
try testing.expectEqual(Binding{
|
||||||
.trigger = .{
|
.trigger = .{
|
||||||
.mods = .{ .shift = true, .ctrl = true },
|
.mods = .{ .shift = true, .ctrl = true },
|
||||||
.key = .a,
|
.key = .{ .translated = .a },
|
||||||
},
|
},
|
||||||
.action = .{ .ignore = {} },
|
.action = .{ .ignore = {} },
|
||||||
}, try parse("shift+ctrl+a=ignore"));
|
}, try parse("shift+ctrl+a=ignore"));
|
||||||
@ -755,7 +827,7 @@ test "parse: triggers" {
|
|||||||
try testing.expectEqual(Binding{
|
try testing.expectEqual(Binding{
|
||||||
.trigger = .{
|
.trigger = .{
|
||||||
.mods = .{ .shift = true },
|
.mods = .{ .shift = true },
|
||||||
.key = .a,
|
.key = .{ .translated = .a },
|
||||||
},
|
},
|
||||||
.action = .{ .ignore = {} },
|
.action = .{ .ignore = {} },
|
||||||
}, try parse("a+shift=ignore"));
|
}, try parse("a+shift=ignore"));
|
||||||
@ -764,17 +836,25 @@ test "parse: triggers" {
|
|||||||
try testing.expectEqual(Binding{
|
try testing.expectEqual(Binding{
|
||||||
.trigger = .{
|
.trigger = .{
|
||||||
.mods = .{ .shift = true },
|
.mods = .{ .shift = true },
|
||||||
.key = .a,
|
.key = .{ .physical = .a },
|
||||||
.physical = true,
|
|
||||||
},
|
},
|
||||||
.action = .{ .ignore = {} },
|
.action = .{ .ignore = {} },
|
||||||
}, try parse("shift+physical:a=ignore"));
|
}, try parse("shift+physical:a=ignore"));
|
||||||
|
|
||||||
|
// unicode keys
|
||||||
|
try testing.expectEqual(Binding{
|
||||||
|
.trigger = .{
|
||||||
|
.mods = .{ .shift = true },
|
||||||
|
.key = .{ .unicode = 'ö' },
|
||||||
|
},
|
||||||
|
.action = .{ .ignore = {} },
|
||||||
|
}, try parse("shift+ö=ignore"));
|
||||||
|
|
||||||
// unconsumed keys
|
// unconsumed keys
|
||||||
try testing.expectEqual(Binding{
|
try testing.expectEqual(Binding{
|
||||||
.trigger = .{
|
.trigger = .{
|
||||||
.mods = .{ .shift = true },
|
.mods = .{ .shift = true },
|
||||||
.key = .a,
|
.key = .{ .translated = .a },
|
||||||
},
|
},
|
||||||
.action = .{ .ignore = {} },
|
.action = .{ .ignore = {} },
|
||||||
.consumed = false,
|
.consumed = false,
|
||||||
@ -784,8 +864,7 @@ test "parse: triggers" {
|
|||||||
try testing.expectEqual(Binding{
|
try testing.expectEqual(Binding{
|
||||||
.trigger = .{
|
.trigger = .{
|
||||||
.mods = .{ .shift = true },
|
.mods = .{ .shift = true },
|
||||||
.key = .a,
|
.key = .{ .physical = .a },
|
||||||
.physical = true,
|
|
||||||
},
|
},
|
||||||
.action = .{ .ignore = {} },
|
.action = .{ .ignore = {} },
|
||||||
.consumed = false,
|
.consumed = false,
|
||||||
@ -807,14 +886,14 @@ test "parse: modifier aliases" {
|
|||||||
try testing.expectEqual(Binding{
|
try testing.expectEqual(Binding{
|
||||||
.trigger = .{
|
.trigger = .{
|
||||||
.mods = .{ .super = true },
|
.mods = .{ .super = true },
|
||||||
.key = .a,
|
.key = .{ .translated = .a },
|
||||||
},
|
},
|
||||||
.action = .{ .ignore = {} },
|
.action = .{ .ignore = {} },
|
||||||
}, try parse("cmd+a=ignore"));
|
}, try parse("cmd+a=ignore"));
|
||||||
try testing.expectEqual(Binding{
|
try testing.expectEqual(Binding{
|
||||||
.trigger = .{
|
.trigger = .{
|
||||||
.mods = .{ .super = true },
|
.mods = .{ .super = true },
|
||||||
.key = .a,
|
.key = .{ .translated = .a },
|
||||||
},
|
},
|
||||||
.action = .{ .ignore = {} },
|
.action = .{ .ignore = {} },
|
||||||
}, try parse("command+a=ignore"));
|
}, try parse("command+a=ignore"));
|
||||||
@ -822,14 +901,14 @@ test "parse: modifier aliases" {
|
|||||||
try testing.expectEqual(Binding{
|
try testing.expectEqual(Binding{
|
||||||
.trigger = .{
|
.trigger = .{
|
||||||
.mods = .{ .alt = true },
|
.mods = .{ .alt = true },
|
||||||
.key = .a,
|
.key = .{ .translated = .a },
|
||||||
},
|
},
|
||||||
.action = .{ .ignore = {} },
|
.action = .{ .ignore = {} },
|
||||||
}, try parse("opt+a=ignore"));
|
}, try parse("opt+a=ignore"));
|
||||||
try testing.expectEqual(Binding{
|
try testing.expectEqual(Binding{
|
||||||
.trigger = .{
|
.trigger = .{
|
||||||
.mods = .{ .alt = true },
|
.mods = .{ .alt = true },
|
||||||
.key = .a,
|
.key = .{ .translated = .a },
|
||||||
},
|
},
|
||||||
.action = .{ .ignore = {} },
|
.action = .{ .ignore = {} },
|
||||||
}, try parse("option+a=ignore"));
|
}, try parse("option+a=ignore"));
|
||||||
@ -837,7 +916,7 @@ test "parse: modifier aliases" {
|
|||||||
try testing.expectEqual(Binding{
|
try testing.expectEqual(Binding{
|
||||||
.trigger = .{
|
.trigger = .{
|
||||||
.mods = .{ .ctrl = true },
|
.mods = .{ .ctrl = true },
|
||||||
.key = .a,
|
.key = .{ .translated = .a },
|
||||||
},
|
},
|
||||||
.action = .{ .ignore = {} },
|
.action = .{ .ignore = {} },
|
||||||
}, try parse("control+a=ignore"));
|
}, try parse("control+a=ignore"));
|
||||||
@ -855,7 +934,10 @@ test "parse: action no parameters" {
|
|||||||
|
|
||||||
// no parameters
|
// no parameters
|
||||||
try testing.expectEqual(
|
try testing.expectEqual(
|
||||||
Binding{ .trigger = .{ .key = .a }, .action = .{ .ignore = {} } },
|
Binding{
|
||||||
|
.trigger = .{ .key = .{ .translated = .a } },
|
||||||
|
.action = .{ .ignore = {} },
|
||||||
|
},
|
||||||
try parse("a=ignore"),
|
try parse("a=ignore"),
|
||||||
);
|
);
|
||||||
try testing.expectError(Error.InvalidFormat, parse("a=ignore:A"));
|
try testing.expectError(Error.InvalidFormat, parse("a=ignore:A"));
|
||||||
@ -949,24 +1031,24 @@ test "set: maintains reverse mapping" {
|
|||||||
var s: Set = .{};
|
var s: Set = .{};
|
||||||
defer s.deinit(alloc);
|
defer s.deinit(alloc);
|
||||||
|
|
||||||
try s.put(alloc, .{ .key = .a }, .{ .new_window = {} });
|
try s.put(alloc, .{ .key = .{ .translated = .a } }, .{ .new_window = {} });
|
||||||
{
|
{
|
||||||
const trigger = s.getTrigger(.{ .new_window = {} }).?;
|
const trigger = s.getTrigger(.{ .new_window = {} }).?;
|
||||||
try testing.expect(trigger.key == .a);
|
try testing.expect(trigger.key.translated == .a);
|
||||||
}
|
}
|
||||||
|
|
||||||
// should be most recent
|
// should be most recent
|
||||||
try s.put(alloc, .{ .key = .b }, .{ .new_window = {} });
|
try s.put(alloc, .{ .key = .{ .translated = .b } }, .{ .new_window = {} });
|
||||||
{
|
{
|
||||||
const trigger = s.getTrigger(.{ .new_window = {} }).?;
|
const trigger = s.getTrigger(.{ .new_window = {} }).?;
|
||||||
try testing.expect(trigger.key == .b);
|
try testing.expect(trigger.key.translated == .b);
|
||||||
}
|
}
|
||||||
|
|
||||||
// removal should replace
|
// removal should replace
|
||||||
s.remove(.{ .key = .b });
|
s.remove(.{ .key = .{ .translated = .b } });
|
||||||
{
|
{
|
||||||
const trigger = s.getTrigger(.{ .new_window = {} }).?;
|
const trigger = s.getTrigger(.{ .new_window = {} }).?;
|
||||||
try testing.expect(trigger.key == .a);
|
try testing.expect(trigger.key.translated == .a);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -977,14 +1059,14 @@ test "set: overriding a mapping updates reverse" {
|
|||||||
var s: Set = .{};
|
var s: Set = .{};
|
||||||
defer s.deinit(alloc);
|
defer s.deinit(alloc);
|
||||||
|
|
||||||
try s.put(alloc, .{ .key = .a }, .{ .new_window = {} });
|
try s.put(alloc, .{ .key = .{ .translated = .a } }, .{ .new_window = {} });
|
||||||
{
|
{
|
||||||
const trigger = s.getTrigger(.{ .new_window = {} }).?;
|
const trigger = s.getTrigger(.{ .new_window = {} }).?;
|
||||||
try testing.expect(trigger.key == .a);
|
try testing.expect(trigger.key.translated == .a);
|
||||||
}
|
}
|
||||||
|
|
||||||
// should be most recent
|
// should be most recent
|
||||||
try s.put(alloc, .{ .key = .a }, .{ .new_tab = {} });
|
try s.put(alloc, .{ .key = .{ .translated = .a } }, .{ .new_tab = {} });
|
||||||
{
|
{
|
||||||
const trigger = s.getTrigger(.{ .new_window = {} });
|
const trigger = s.getTrigger(.{ .new_window = {} });
|
||||||
try testing.expect(trigger == null);
|
try testing.expect(trigger == null);
|
||||||
@ -998,12 +1080,12 @@ test "set: consumed state" {
|
|||||||
var s: Set = .{};
|
var s: Set = .{};
|
||||||
defer s.deinit(alloc);
|
defer s.deinit(alloc);
|
||||||
|
|
||||||
try s.put(alloc, .{ .key = .a }, .{ .new_window = {} });
|
try s.put(alloc, .{ .key = .{ .translated = .a } }, .{ .new_window = {} });
|
||||||
try testing.expect(s.getConsumed(.{ .key = .a }));
|
try testing.expect(s.getConsumed(.{ .key = .{ .translated = .a } }));
|
||||||
|
|
||||||
try s.putUnconsumed(alloc, .{ .key = .a }, .{ .new_window = {} });
|
try s.putUnconsumed(alloc, .{ .key = .{ .translated = .a } }, .{ .new_window = {} });
|
||||||
try testing.expect(!s.getConsumed(.{ .key = .a }));
|
try testing.expect(!s.getConsumed(.{ .key = .{ .translated = .a } }));
|
||||||
|
|
||||||
try s.put(alloc, .{ .key = .a }, .{ .new_window = {} });
|
try s.put(alloc, .{ .key = .{ .translated = .a } }, .{ .new_window = {} });
|
||||||
try testing.expect(s.getConsumed(.{ .key = .a }));
|
try testing.expect(s.getConsumed(.{ .key = .{ .translated = .a } }));
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user