mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-17 17:26:09 +03:00
Merge pull request #133 from mitchellh/translate-keys
Translate keys according to keyboard layout (partial fix)
This commit is contained in:
@ -261,7 +261,7 @@ void ghostty_surface_refresh(ghostty_surface_t);
|
|||||||
void ghostty_surface_set_content_scale(ghostty_surface_t, double, double);
|
void ghostty_surface_set_content_scale(ghostty_surface_t, double, double);
|
||||||
void ghostty_surface_set_focus(ghostty_surface_t, bool);
|
void ghostty_surface_set_focus(ghostty_surface_t, bool);
|
||||||
void ghostty_surface_set_size(ghostty_surface_t, uint32_t, uint32_t);
|
void ghostty_surface_set_size(ghostty_surface_t, uint32_t, uint32_t);
|
||||||
void ghostty_surface_key(ghostty_surface_t, ghostty_input_action_e, ghostty_input_key_e, ghostty_input_mods_e);
|
void ghostty_surface_key(ghostty_surface_t, ghostty_input_action_e, ghostty_input_key_e, ghostty_input_key_e, ghostty_input_mods_e);
|
||||||
void ghostty_surface_char(ghostty_surface_t, uint32_t);
|
void ghostty_surface_char(ghostty_surface_t, uint32_t);
|
||||||
void ghostty_surface_mouse_button(ghostty_surface_t, ghostty_input_mouse_state_e, ghostty_input_mouse_button_e, ghostty_input_mods_e);
|
void ghostty_surface_mouse_button(ghostty_surface_t, ghostty_input_mouse_state_e, ghostty_input_mouse_button_e, ghostty_input_mods_e);
|
||||||
void ghostty_surface_mouse_pos(ghostty_surface_t, double, double);
|
void ghostty_surface_mouse_pos(ghostty_surface_t, double, double);
|
||||||
|
@ -75,3 +75,6 @@ extension Ghostty.Notification {
|
|||||||
static let ghosttyFocusSplit = Notification.Name("com.mitchellh.ghostty.focusSplit")
|
static let ghosttyFocusSplit = Notification.Name("com.mitchellh.ghostty.focusSplit")
|
||||||
static let SplitDirectionKey = ghosttyFocusSplit.rawValue
|
static let SplitDirectionKey = ghosttyFocusSplit.rawValue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Make the input enum hashable.
|
||||||
|
extension ghostty_input_key_e : Hashable {}
|
||||||
|
@ -302,20 +302,39 @@ extension Ghostty {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func keyDown(with event: NSEvent) {
|
override func keyDown(with event: NSEvent) {
|
||||||
guard let surface = self.surface else { return }
|
|
||||||
let key = Self.keycodes[event.keyCode] ?? GHOSTTY_KEY_INVALID
|
|
||||||
let mods = Self.translateFlags(event.modifierFlags)
|
|
||||||
let action = event.isARepeat ? GHOSTTY_ACTION_REPEAT : GHOSTTY_ACTION_PRESS
|
let action = event.isARepeat ? GHOSTTY_ACTION_REPEAT : GHOSTTY_ACTION_PRESS
|
||||||
ghostty_surface_key(surface, action, key, mods)
|
keyAction(action, event: event)
|
||||||
|
|
||||||
self.interpretKeyEvents([event])
|
self.interpretKeyEvents([event])
|
||||||
}
|
}
|
||||||
|
|
||||||
override func keyUp(with event: NSEvent) {
|
override func keyUp(with event: NSEvent) {
|
||||||
|
keyAction(GHOSTTY_ACTION_RELEASE, event: event)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func keyAction(_ action: ghostty_input_action_e, event: NSEvent) {
|
||||||
guard let surface = self.surface else { return }
|
guard let surface = self.surface else { return }
|
||||||
let key = Self.keycodes[event.keyCode] ?? GHOSTTY_KEY_INVALID
|
|
||||||
let mods = Self.translateFlags(event.modifierFlags)
|
let mods = Self.translateFlags(event.modifierFlags)
|
||||||
ghostty_surface_key(surface, GHOSTTY_ACTION_RELEASE, key, mods)
|
let unmapped_key = Self.keycodes[event.keyCode] ?? GHOSTTY_KEY_INVALID
|
||||||
|
|
||||||
|
// We translate the key to the localized keyboard layout. However, we only support
|
||||||
|
// ASCII characters to make our translation easier across platforms. This is something
|
||||||
|
// we want to make a lot more robust in the future, so this will hopefully change.
|
||||||
|
// For now, this makes most keyboard layouts work, and for those that don't, they can
|
||||||
|
// use physical keycode mappings.
|
||||||
|
let key = {
|
||||||
|
if let str = event.characters(byApplyingModifiers: .init(rawValue: 0)) {
|
||||||
|
if str.utf8.count == 1, let firstByte = str.utf8.first {
|
||||||
|
if let translatedKey = Self.ascii[firstByte] {
|
||||||
|
return translatedKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return unmapped_key
|
||||||
|
}()
|
||||||
|
|
||||||
|
ghostty_surface_key(surface, action, key, unmapped_key, mods)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: NSTextInputClient
|
// MARK: NSTextInputClient
|
||||||
@ -536,6 +555,89 @@ extension Ghostty {
|
|||||||
0x43: GHOSTTY_KEY_KP_MULTIPLY,
|
0x43: GHOSTTY_KEY_KP_MULTIPLY,
|
||||||
0x4E: GHOSTTY_KEY_KP_SUBTRACT,
|
0x4E: GHOSTTY_KEY_KP_SUBTRACT,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
static let ascii: [UInt8 : ghostty_input_key_e] = [
|
||||||
|
// 0-9
|
||||||
|
0x30: GHOSTTY_KEY_ZERO,
|
||||||
|
0x31: GHOSTTY_KEY_ONE,
|
||||||
|
0x32: GHOSTTY_KEY_TWO,
|
||||||
|
0x33: GHOSTTY_KEY_THREE,
|
||||||
|
0x34: GHOSTTY_KEY_FOUR,
|
||||||
|
0x35: GHOSTTY_KEY_FIVE,
|
||||||
|
0x36: GHOSTTY_KEY_SIX,
|
||||||
|
0x37: GHOSTTY_KEY_SEVEN,
|
||||||
|
0x38: GHOSTTY_KEY_EIGHT,
|
||||||
|
0x39: GHOSTTY_KEY_NINE,
|
||||||
|
|
||||||
|
// A-Z
|
||||||
|
0x41: GHOSTTY_KEY_A,
|
||||||
|
0x42: GHOSTTY_KEY_B,
|
||||||
|
0x43: GHOSTTY_KEY_C,
|
||||||
|
0x44: GHOSTTY_KEY_D,
|
||||||
|
0x45: GHOSTTY_KEY_E,
|
||||||
|
0x46: GHOSTTY_KEY_F,
|
||||||
|
0x47: GHOSTTY_KEY_G,
|
||||||
|
0x48: GHOSTTY_KEY_H,
|
||||||
|
0x49: GHOSTTY_KEY_I,
|
||||||
|
0x4A: GHOSTTY_KEY_J,
|
||||||
|
0x4B: GHOSTTY_KEY_K,
|
||||||
|
0x4C: GHOSTTY_KEY_L,
|
||||||
|
0x4D: GHOSTTY_KEY_M,
|
||||||
|
0x4E: GHOSTTY_KEY_N,
|
||||||
|
0x4F: GHOSTTY_KEY_O,
|
||||||
|
0x50: GHOSTTY_KEY_P,
|
||||||
|
0x51: GHOSTTY_KEY_Q,
|
||||||
|
0x52: GHOSTTY_KEY_R,
|
||||||
|
0x53: GHOSTTY_KEY_S,
|
||||||
|
0x54: GHOSTTY_KEY_T,
|
||||||
|
0x55: GHOSTTY_KEY_U,
|
||||||
|
0x56: GHOSTTY_KEY_V,
|
||||||
|
0x57: GHOSTTY_KEY_W,
|
||||||
|
0x58: GHOSTTY_KEY_X,
|
||||||
|
0x59: GHOSTTY_KEY_Y,
|
||||||
|
0x5A: GHOSTTY_KEY_Z,
|
||||||
|
|
||||||
|
// a-z
|
||||||
|
0x61: GHOSTTY_KEY_A,
|
||||||
|
0x62: GHOSTTY_KEY_B,
|
||||||
|
0x63: GHOSTTY_KEY_C,
|
||||||
|
0x64: GHOSTTY_KEY_D,
|
||||||
|
0x65: GHOSTTY_KEY_E,
|
||||||
|
0x66: GHOSTTY_KEY_F,
|
||||||
|
0x67: GHOSTTY_KEY_G,
|
||||||
|
0x68: GHOSTTY_KEY_H,
|
||||||
|
0x69: GHOSTTY_KEY_I,
|
||||||
|
0x6A: GHOSTTY_KEY_J,
|
||||||
|
0x6B: GHOSTTY_KEY_K,
|
||||||
|
0x6C: GHOSTTY_KEY_L,
|
||||||
|
0x6D: GHOSTTY_KEY_M,
|
||||||
|
0x6E: GHOSTTY_KEY_N,
|
||||||
|
0x6F: GHOSTTY_KEY_O,
|
||||||
|
0x70: GHOSTTY_KEY_P,
|
||||||
|
0x71: GHOSTTY_KEY_Q,
|
||||||
|
0x72: GHOSTTY_KEY_R,
|
||||||
|
0x73: GHOSTTY_KEY_S,
|
||||||
|
0x74: GHOSTTY_KEY_T,
|
||||||
|
0x75: GHOSTTY_KEY_U,
|
||||||
|
0x76: GHOSTTY_KEY_V,
|
||||||
|
0x77: GHOSTTY_KEY_W,
|
||||||
|
0x78: GHOSTTY_KEY_X,
|
||||||
|
0x79: GHOSTTY_KEY_Y,
|
||||||
|
0x7A: GHOSTTY_KEY_Z,
|
||||||
|
|
||||||
|
// Symbols
|
||||||
|
0x27: GHOSTTY_KEY_APOSTROPHE,
|
||||||
|
0x5C: GHOSTTY_KEY_BACKSLASH,
|
||||||
|
0x2C: GHOSTTY_KEY_COMMA,
|
||||||
|
0x3D: GHOSTTY_KEY_EQUAL,
|
||||||
|
0x60: GHOSTTY_KEY_GRAVE_ACCENT,
|
||||||
|
0x5B: GHOSTTY_KEY_LEFT_BRACKET,
|
||||||
|
0x2D: GHOSTTY_KEY_MINUS,
|
||||||
|
0x2E: GHOSTTY_KEY_PERIOD,
|
||||||
|
0x5D: GHOSTTY_KEY_RIGHT_BRACKET,
|
||||||
|
0x3B: GHOSTTY_KEY_SEMICOLON,
|
||||||
|
0x2F: GHOSTTY_KEY_SLASH,
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -850,6 +850,7 @@ pub fn keyCallback(
|
|||||||
self: *Surface,
|
self: *Surface,
|
||||||
action: input.Action,
|
action: input.Action,
|
||||||
key: input.Key,
|
key: input.Key,
|
||||||
|
unmapped_key: input.Key,
|
||||||
mods: input.Mods,
|
mods: input.Mods,
|
||||||
) !void {
|
) !void {
|
||||||
const tracy = trace(@src());
|
const tracy = trace(@src());
|
||||||
@ -870,13 +871,24 @@ pub fn keyCallback(
|
|||||||
self.ignore_char = false;
|
self.ignore_char = false;
|
||||||
|
|
||||||
if (action == .press or action == .repeat) {
|
if (action == .press or action == .repeat) {
|
||||||
const trigger: input.Binding.Trigger = .{
|
const binding_action_: ?input.Binding.Action = action: {
|
||||||
.mods = mods,
|
var trigger: input.Binding.Trigger = .{
|
||||||
.key = key,
|
.mods = mods,
|
||||||
|
.key = key,
|
||||||
|
};
|
||||||
|
//log.warn("BINDING TRIGGER={}", .{trigger});
|
||||||
|
|
||||||
|
const set = self.config.keybind.set;
|
||||||
|
if (set.get(trigger)) |v| break :action v;
|
||||||
|
|
||||||
|
trigger.key = unmapped_key;
|
||||||
|
trigger.unmapped = true;
|
||||||
|
if (set.get(trigger)) |v| break :action v;
|
||||||
|
|
||||||
|
break :action null;
|
||||||
};
|
};
|
||||||
|
|
||||||
//log.warn("BINDING TRIGGER={}", .{trigger});
|
if (binding_action_) |binding_action| {
|
||||||
if (self.config.keybind.set.get(trigger)) |binding_action| {
|
|
||||||
//log.warn("BINDING ACTION={}", .{binding_action});
|
//log.warn("BINDING ACTION={}", .{binding_action});
|
||||||
|
|
||||||
switch (binding_action) {
|
switch (binding_action) {
|
||||||
|
@ -324,10 +324,11 @@ pub const Surface = struct {
|
|||||||
self: *Surface,
|
self: *Surface,
|
||||||
action: input.Action,
|
action: input.Action,
|
||||||
key: input.Key,
|
key: input.Key,
|
||||||
|
unmapped_key: input.Key,
|
||||||
mods: input.Mods,
|
mods: input.Mods,
|
||||||
) void {
|
) void {
|
||||||
// log.warn("key action={} key={} mods={}", .{ action, key, mods });
|
// log.warn("key action={} key={} mods={}", .{ action, key, mods });
|
||||||
self.core_surface.keyCallback(action, key, mods) catch |err| {
|
self.core_surface.keyCallback(action, key, unmapped_key, mods) catch |err| {
|
||||||
log.err("error in key callback err={}", .{err});
|
log.err("error in key callback err={}", .{err});
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@ -460,11 +461,13 @@ pub const CAPI = struct {
|
|||||||
surface: *Surface,
|
surface: *Surface,
|
||||||
action: input.Action,
|
action: input.Action,
|
||||||
key: input.Key,
|
key: input.Key,
|
||||||
|
unmapped_key: input.Key,
|
||||||
mods: c_int,
|
mods: c_int,
|
||||||
) void {
|
) void {
|
||||||
surface.keyCallback(
|
surface.keyCallback(
|
||||||
action,
|
action,
|
||||||
key,
|
key,
|
||||||
|
unmapped_key,
|
||||||
@bitCast(input.Mods, @truncate(u8, @bitCast(c_uint, mods))),
|
@bitCast(input.Mods, @truncate(u8, @bitCast(c_uint, mods))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -683,8 +683,10 @@ pub const Surface = struct {
|
|||||||
=> .invalid,
|
=> .invalid,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: we need to do mapped keybindings
|
||||||
|
|
||||||
const core_win = window.getUserPointer(CoreSurface) orelse return;
|
const core_win = window.getUserPointer(CoreSurface) orelse return;
|
||||||
core_win.keyCallback(action, key, mods) catch |err| {
|
core_win.keyCallback(action, key, key, mods) catch |err| {
|
||||||
log.err("error in key callback err={}", .{err});
|
log.err("error in key callback err={}", .{err});
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
@ -935,7 +935,7 @@ pub const Surface = struct {
|
|||||||
const key = translateKey(keyval);
|
const key = translateKey(keyval);
|
||||||
const mods = translateMods(state);
|
const mods = translateMods(state);
|
||||||
log.debug("key-press code={} key={} mods={}", .{ keycode, key, mods });
|
log.debug("key-press code={} key={} mods={}", .{ keycode, key, mods });
|
||||||
self.core_surface.keyCallback(.press, key, mods) catch |err| {
|
self.core_surface.keyCallback(.press, key, key, mods) catch |err| {
|
||||||
log.err("error in key callback err={}", .{err});
|
log.err("error in key callback err={}", .{err});
|
||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
@ -965,7 +965,7 @@ pub const Surface = struct {
|
|||||||
const key = translateKey(keyval);
|
const key = translateKey(keyval);
|
||||||
const mods = translateMods(state);
|
const mods = translateMods(state);
|
||||||
const self = userdataSelf(ud.?);
|
const self = userdataSelf(ud.?);
|
||||||
self.core_surface.keyCallback(.release, key, mods) catch |err| {
|
self.core_surface.keyCallback(.release, key, key, mods) catch |err| {
|
||||||
log.err("error in key callback err={}", .{err});
|
log.err("error in key callback err={}", .{err});
|
||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
|
@ -53,11 +53,18 @@ pub fn parse(input: []const u8) !Binding {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the key starts with "unmapped" then this is an unmapped key.
|
||||||
|
const unmapped_prefix = "unmapped:";
|
||||||
|
const key_part = if (std.mem.startsWith(u8, part, unmapped_prefix)) key_part: {
|
||||||
|
result.unmapped = true;
|
||||||
|
break :key_part part[unmapped_prefix.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;
|
||||||
inline for (keysInfo.fields) |field| {
|
inline for (keysInfo.fields) |field| {
|
||||||
if (!std.mem.eql(u8, field.name, "invalid")) {
|
if (!std.mem.eql(u8, field.name, "invalid")) {
|
||||||
if (std.mem.eql(u8, 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.key != .invalid) return Error.InvalidFormat;
|
||||||
|
|
||||||
@ -237,11 +244,18 @@ pub const Trigger = struct {
|
|||||||
/// 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 "unmapped" 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.
|
||||||
|
unmapped: bool = 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: Binding) u64 {
|
pub fn hash(self: Binding) 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);
|
std.hash.autoHash(&hasher, self.mods);
|
||||||
|
std.hash.autoHash(&hasher, self.unmapped);
|
||||||
return hasher.final();
|
return hasher.final();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -326,6 +340,16 @@ test "parse: triggers" {
|
|||||||
.action = .{ .ignore = {} },
|
.action = .{ .ignore = {} },
|
||||||
}, try parse("a+shift=ignore"));
|
}, try parse("a+shift=ignore"));
|
||||||
|
|
||||||
|
// unmapped keys
|
||||||
|
try testing.expectEqual(Binding{
|
||||||
|
.trigger = .{
|
||||||
|
.mods = .{ .shift = true },
|
||||||
|
.key = .a,
|
||||||
|
.unmapped = true,
|
||||||
|
},
|
||||||
|
.action = .{ .ignore = {} },
|
||||||
|
}, try parse("shift+unmapped:a=ignore"));
|
||||||
|
|
||||||
// invalid key
|
// invalid key
|
||||||
try testing.expectError(Error.InvalidFormat, parse("foo=ignore"));
|
try testing.expectError(Error.InvalidFormat, parse("foo=ignore"));
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user