mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +03:00
Merge pull request #292 from mitchellh/fixterms
Implement the "fixterms" keyboard input spec
This commit is contained in:
366
src/Surface.zig
366
src/Surface.zig
@ -987,189 +987,29 @@ pub fn preeditCallback(self: *Surface, preedit: ?u21) !void {
|
|||||||
try self.queueRender();
|
try self.queueRender();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn charCallback(
|
/// Called for any key events. This handles keybindings, encoding and
|
||||||
self: *Surface,
|
/// sending to the termianl, etc. The return value is true if the key
|
||||||
codepoint: u21,
|
/// was handled and false if it was not.
|
||||||
mods: input.Mods,
|
|
||||||
) !void {
|
|
||||||
const tracy = trace(@src());
|
|
||||||
defer tracy.end();
|
|
||||||
|
|
||||||
// Dev Mode
|
|
||||||
if (DevMode.enabled and DevMode.instance.visible) {
|
|
||||||
// If the event was handled by imgui, ignore it.
|
|
||||||
if (imgui.IO.get()) |io| {
|
|
||||||
if (io.cval().WantCaptureKeyboard) {
|
|
||||||
try self.queueRender();
|
|
||||||
}
|
|
||||||
} else |_| {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Critical area
|
|
||||||
const critical: struct {
|
|
||||||
alt_esc_prefix: bool,
|
|
||||||
modify_other_keys: bool,
|
|
||||||
} = critical: {
|
|
||||||
self.renderer_state.mutex.lock();
|
|
||||||
defer self.renderer_state.mutex.unlock();
|
|
||||||
|
|
||||||
// Clear the selection if we have one.
|
|
||||||
if (self.io.terminal.screen.selection != null) {
|
|
||||||
self.setSelection(null);
|
|
||||||
try self.queueRender();
|
|
||||||
}
|
|
||||||
|
|
||||||
// We want to scroll to the bottom
|
|
||||||
// TODO: detect if we're at the bottom to avoid the render call here.
|
|
||||||
try self.io.terminal.scrollViewport(.{ .bottom = {} });
|
|
||||||
|
|
||||||
break :critical .{
|
|
||||||
.alt_esc_prefix = self.io.terminal.modes.get(.alt_esc_prefix),
|
|
||||||
.modify_other_keys = self.io.terminal.flags.modify_other_keys_2,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Where we're going to write any data. Any data we write has to
|
|
||||||
// fit into the fixed size array so we just define it up front.
|
|
||||||
var data: termio.Message.WriteReq.Small.Array = undefined;
|
|
||||||
|
|
||||||
// In modify other keys state 2, we send the CSI 27 sequence
|
|
||||||
// for any char with a modifier. Ctrl sequences like Ctrl+A
|
|
||||||
// are handled in keyCallback and should never have reached this
|
|
||||||
// point.
|
|
||||||
if (critical.modify_other_keys) {
|
|
||||||
// This copies xterm's `ModifyOtherKeys` function that returns
|
|
||||||
// whether modify other keys should be encoded for the given
|
|
||||||
// input.
|
|
||||||
const should_modify = should_modify: {
|
|
||||||
// xterm IsControlInput
|
|
||||||
if (codepoint >= 0x40 and codepoint <= 0x7F)
|
|
||||||
break :should_modify true;
|
|
||||||
|
|
||||||
// If we have anything other than shift pressed, encode.
|
|
||||||
var mods_no_shift = mods;
|
|
||||||
mods_no_shift.shift = false;
|
|
||||||
if (!mods_no_shift.empty()) break :should_modify true;
|
|
||||||
|
|
||||||
// We only have shift pressed. We only allow space.
|
|
||||||
if (codepoint == ' ') break :should_modify true;
|
|
||||||
|
|
||||||
// This logic isn't complete but I don't fully understand
|
|
||||||
// the rest so I'm going to wait until we can have a
|
|
||||||
// reasonable test scenario.
|
|
||||||
break :should_modify false;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (should_modify) {
|
|
||||||
for (input.function_keys.modifiers, 2..) |modset, code| {
|
|
||||||
if (!mods.equal(modset)) continue;
|
|
||||||
|
|
||||||
const resp = try std.fmt.bufPrint(
|
|
||||||
&data,
|
|
||||||
"\x1B[27;{};{}~",
|
|
||||||
.{ code, codepoint },
|
|
||||||
);
|
|
||||||
_ = self.io_thread.mailbox.push(.{
|
|
||||||
.write_small = .{
|
|
||||||
.data = data,
|
|
||||||
.len = @intCast(resp.len),
|
|
||||||
},
|
|
||||||
}, .{ .forever = {} });
|
|
||||||
try self.io_thread.wakeup.notify();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prefix our data with ESC if we have alt pressed.
|
|
||||||
var i: u8 = 0;
|
|
||||||
if (mods.alt) alt: {
|
|
||||||
// If the terminal explicitly disabled this feature using mode 1036,
|
|
||||||
// then we don't send the prefix.
|
|
||||||
if (!critical.alt_esc_prefix) {
|
|
||||||
log.debug("alt_esc_prefix disabled with mode, not sending esc prefix", .{});
|
|
||||||
break :alt;
|
|
||||||
}
|
|
||||||
|
|
||||||
// On macOS, we have to opt-in to using alt because option
|
|
||||||
// by default is a unicode character sequence.
|
|
||||||
if (comptime builtin.target.isDarwin()) {
|
|
||||||
switch (self.config.macos_option_as_alt) {
|
|
||||||
.false => break :alt,
|
|
||||||
.true => {},
|
|
||||||
.left => if (mods.sides.alt != .left) break :alt,
|
|
||||||
.right => if (mods.sides.alt != .right) break :alt,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data[i] = 0x1b;
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const len = try std.unicode.utf8Encode(codepoint, data[i..]);
|
|
||||||
_ = self.io_thread.mailbox.push(.{
|
|
||||||
.write_small = .{
|
|
||||||
.data = data,
|
|
||||||
.len = len + i,
|
|
||||||
},
|
|
||||||
}, .{ .forever = {} });
|
|
||||||
|
|
||||||
// After sending all our messages we have to notify our IO thread
|
|
||||||
try self.io_thread.wakeup.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Called for a single key event.
|
|
||||||
///
|
|
||||||
/// This will return true if the key was handled/consumed. In that case,
|
|
||||||
/// the caller doesn't need to call a subsequent `charCallback` for the
|
|
||||||
/// same event. However, the caller can call `charCallback` if they want,
|
|
||||||
/// the surface will retain state to ensure the event is ignored.
|
|
||||||
pub fn keyCallback(
|
pub fn keyCallback(
|
||||||
self: *Surface,
|
self: *Surface,
|
||||||
action: input.Action,
|
event: input.KeyEvent,
|
||||||
key: input.Key,
|
|
||||||
physical_key: input.Key,
|
|
||||||
mods: input.Mods,
|
|
||||||
) !bool {
|
) !bool {
|
||||||
const tracy = trace(@src());
|
// log.debug("keyCallback event={}", .{event});
|
||||||
defer tracy.end();
|
|
||||||
|
|
||||||
// log.warn("KEY CALLBACK action={} key={} physical_key={} mods={}", .{
|
// Before encoding, we see if we have any keybindings for this
|
||||||
// action,
|
// key. Those always intercept before any encoding tasks.
|
||||||
// key,
|
if (event.action == .press or event.action == .repeat) {
|
||||||
// physical_key,
|
const binding_mods = event.effectiveMods().binding();
|
||||||
// mods,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// Dev Mode
|
|
||||||
if (DevMode.enabled and DevMode.instance.visible) {
|
|
||||||
// If the event was handled by imgui, ignore it.
|
|
||||||
if (imgui.IO.get()) |io| {
|
|
||||||
if (io.cval().WantCaptureKeyboard) {
|
|
||||||
try self.queueRender();
|
|
||||||
}
|
|
||||||
} else |_| {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We only handle press events
|
|
||||||
if (action != .press and action != .repeat) return false;
|
|
||||||
|
|
||||||
// Mods for bindings never include caps/num lock.
|
|
||||||
const binding_mods = mods.binding();
|
|
||||||
|
|
||||||
// Check if we're processing a binding first. If so, that negates
|
|
||||||
// any further key processing.
|
|
||||||
{
|
|
||||||
const binding_action_: ?input.Binding.Action = action: {
|
const binding_action_: ?input.Binding.Action = action: {
|
||||||
var trigger: input.Binding.Trigger = .{
|
var trigger: input.Binding.Trigger = .{
|
||||||
.mods = binding_mods,
|
.mods = binding_mods,
|
||||||
.key = key,
|
.key = event.key,
|
||||||
};
|
};
|
||||||
|
|
||||||
const set = self.config.keybind.set;
|
const set = self.config.keybind.set;
|
||||||
if (set.get(trigger)) |v| break :action v;
|
if (set.get(trigger)) |v| break :action v;
|
||||||
|
|
||||||
trigger.key = physical_key;
|
trigger.key = event.physical_key;
|
||||||
trigger.physical = true;
|
trigger.physical = true;
|
||||||
if (set.get(trigger)) |v| break :action v;
|
if (set.get(trigger)) |v| break :action v;
|
||||||
|
|
||||||
@ -1183,164 +1023,46 @@ pub fn keyCallback(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We'll need to know these values here on.
|
// No binding, so we have to perform an encoding task. This
|
||||||
self.renderer_state.mutex.lock();
|
// may still result in no encoding. Under different modes and
|
||||||
const cursor_key_application = self.io.terminal.modes.get(.cursor_keys);
|
// inputs there are many keybindings that result in no encoding
|
||||||
const keypad_key_application = self.io.terminal.modes.get(.keypad_keys);
|
// whatsoever.
|
||||||
const modify_other_keys = self.io.terminal.flags.modify_other_keys_2;
|
const enc: input.KeyEncoder = enc: {
|
||||||
self.renderer_state.mutex.unlock();
|
self.renderer_state.mutex.lock();
|
||||||
|
defer self.renderer_state.mutex.unlock();
|
||||||
// Check if we're processing a function key.
|
const t = &self.io.terminal;
|
||||||
for (input.function_keys.keys.get(key)) |entry| {
|
break :enc .{
|
||||||
switch (entry.cursor) {
|
.event = event,
|
||||||
.any => {},
|
.alt_esc_prefix = t.modes.get(.alt_esc_prefix),
|
||||||
.normal => if (cursor_key_application) continue,
|
.cursor_key_application = t.modes.get(.cursor_keys),
|
||||||
.application => if (!cursor_key_application) continue,
|
.keypad_key_application = t.modes.get(.keypad_keys),
|
||||||
}
|
.modify_other_keys_state_2 = t.flags.modify_other_keys_2,
|
||||||
|
|
||||||
switch (entry.keypad) {
|
|
||||||
.any => {},
|
|
||||||
.normal => if (keypad_key_application) continue,
|
|
||||||
.application => if (!keypad_key_application) continue,
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (entry.modify_other_keys) {
|
|
||||||
.any => {},
|
|
||||||
.set => if (modify_other_keys) continue,
|
|
||||||
.set_other => if (!modify_other_keys) continue,
|
|
||||||
}
|
|
||||||
|
|
||||||
const mods_int = binding_mods.int();
|
|
||||||
const entry_mods_int = entry.mods.int();
|
|
||||||
if (entry_mods_int == 0) {
|
|
||||||
if (mods_int != 0 and !entry.mods_empty_is_any) continue;
|
|
||||||
// mods are either empty, or empty means any so we allow it.
|
|
||||||
} else if (entry_mods_int != mods_int) {
|
|
||||||
// any set mods require an exact match
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// log.debug("function key match: {}", .{entry});
|
|
||||||
|
|
||||||
// We found a match, send the sequence and return we as handled.
|
|
||||||
var data: termio.Message.WriteReq.Small.Array = undefined;
|
|
||||||
@memcpy(data[0..entry.sequence.len], entry.sequence);
|
|
||||||
_ = self.io_thread.mailbox.push(.{
|
|
||||||
.write_small = .{
|
|
||||||
.data = data,
|
|
||||||
.len = @intCast(entry.sequence.len),
|
|
||||||
},
|
|
||||||
}, .{ .forever = {} });
|
|
||||||
try self.io_thread.wakeup.notify();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have alt pressed, we're going to prefix any of the
|
|
||||||
// translations below with ESC (0x1B).
|
|
||||||
const alt = binding_mods.alt;
|
|
||||||
const unalt_mods = unalt_mods: {
|
|
||||||
var unalt_mods = binding_mods;
|
|
||||||
unalt_mods.alt = false;
|
|
||||||
break :unalt_mods unalt_mods;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle non-printables
|
|
||||||
const char: u8 = char: {
|
|
||||||
const mods_int = unalt_mods.int();
|
|
||||||
const ctrl_only = (input.Mods{ .ctrl = true }).int();
|
|
||||||
|
|
||||||
// If we're only pressing control, check if this is a character
|
|
||||||
// we convert to a non-printable. The best table I've found for
|
|
||||||
// this is:
|
|
||||||
// https://sw.kovidgoyal.net/kitty/keyboard-protocol/#legacy-ctrl-mapping-of-ascii-keys
|
|
||||||
//
|
|
||||||
// Note that depending on the apprt, these might be handled as
|
|
||||||
// composed characters. But not all app runtimes will do this;
|
|
||||||
// some only compose printable characters. So we manually handle
|
|
||||||
// this here.
|
|
||||||
if (mods_int != ctrl_only) break :char 0;
|
|
||||||
break :char switch (key) {
|
|
||||||
.space => 0,
|
|
||||||
.slash => 0x1F,
|
|
||||||
.zero => 0x30,
|
|
||||||
.one => 0x31,
|
|
||||||
.two => 0x00,
|
|
||||||
.three => 0x1B,
|
|
||||||
.four => 0x1C,
|
|
||||||
.five => 0x1D,
|
|
||||||
.six => 0x1E,
|
|
||||||
.seven => 0x1F,
|
|
||||||
.eight => 0x7F,
|
|
||||||
.nine => 0x39,
|
|
||||||
.backslash => 0x1C,
|
|
||||||
.left_bracket => 0x1B,
|
|
||||||
.right_bracket => 0x1D,
|
|
||||||
.a => 0x01,
|
|
||||||
.b => 0x02,
|
|
||||||
.c => 0x03,
|
|
||||||
.d => 0x04,
|
|
||||||
.e => 0x05,
|
|
||||||
.f => 0x06,
|
|
||||||
.g => 0x07,
|
|
||||||
.h => 0x08,
|
|
||||||
.i => 0x09,
|
|
||||||
.j => 0x0A,
|
|
||||||
.k => 0x0B,
|
|
||||||
.l => 0x0C,
|
|
||||||
.m => 0x0D,
|
|
||||||
.n => 0x0E,
|
|
||||||
.o => 0x0F,
|
|
||||||
.p => 0x10,
|
|
||||||
.q => 0x11,
|
|
||||||
.r => 0x12,
|
|
||||||
.s => 0x13,
|
|
||||||
.t => 0x14,
|
|
||||||
.u => 0x15,
|
|
||||||
.v => 0x16,
|
|
||||||
.w => 0x17,
|
|
||||||
.x => 0x18,
|
|
||||||
.y => 0x19,
|
|
||||||
.z => 0x1A,
|
|
||||||
else => 0,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
if (char > 0) {
|
|
||||||
// Ask our IO thread to write the data
|
|
||||||
var data: termio.Message.WriteReq.Small.Array = undefined;
|
|
||||||
|
|
||||||
// Write our data. If we need to alt-prefix we add that first.
|
var data: termio.Message.WriteReq.Small.Array = undefined;
|
||||||
var i: u8 = 0;
|
const seq = try enc.legacy(&data);
|
||||||
if (alt) {
|
if (seq.len == 0) return false;
|
||||||
data[i] = 0x1B;
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
data[i] = @intCast(char);
|
|
||||||
i += 1;
|
|
||||||
|
|
||||||
_ = self.io_thread.mailbox.push(.{
|
_ = self.io_thread.mailbox.push(.{
|
||||||
.write_small = .{
|
.write_small = .{
|
||||||
.data = data,
|
.data = data,
|
||||||
.len = i,
|
.len = @intCast(seq.len),
|
||||||
},
|
},
|
||||||
}, .{ .forever = {} });
|
}, .{ .forever = {} });
|
||||||
|
try self.io_thread.wakeup.notify();
|
||||||
|
|
||||||
// After sending all our messages we have to notify our IO thread
|
// If we have a sequence to emit then we always want to clear the
|
||||||
try self.io_thread.wakeup.notify();
|
// selection and scroll to the bottom.
|
||||||
|
{
|
||||||
// Control charactesr trigger a scroll
|
self.renderer_state.mutex.lock();
|
||||||
{
|
defer self.renderer_state.mutex.unlock();
|
||||||
self.renderer_state.mutex.lock();
|
self.setSelection(null);
|
||||||
defer self.renderer_state.mutex.unlock();
|
try self.io.terminal.scrollViewport(.{ .bottom = {} });
|
||||||
self.scrollToBottom() catch |err| {
|
try self.queueRender();
|
||||||
log.warn("error scrolling to bottom err={}", .{err});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn focusCallback(self: *Surface, focused: bool) !void {
|
pub fn focusCallback(self: *Surface, focused: bool) !void {
|
||||||
|
@ -387,8 +387,8 @@ pub const Surface = struct {
|
|||||||
keycode: u32,
|
keycode: u32,
|
||||||
mods: input.Mods,
|
mods: input.Mods,
|
||||||
) !void {
|
) !void {
|
||||||
// We don't handle release events because we don't use them yet.
|
// True if this is a key down event
|
||||||
if (action != .press and action != .repeat) return;
|
const is_down = action == .press or action == .repeat;
|
||||||
|
|
||||||
// If we're on macOS and we have macos-option-as-alt enabled,
|
// If we're on macOS and we have macos-option-as-alt enabled,
|
||||||
// then we strip the alt modifier from the mods for translation.
|
// then we strip the alt modifier from the mods for translation.
|
||||||
@ -405,22 +405,58 @@ pub const Surface = struct {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// On macOS we strip ctrl because UCKeyTranslate
|
||||||
|
// converts to the masked values (i.e. ctrl+c becomes 3)
|
||||||
|
// and we don't want that behavior.
|
||||||
|
//
|
||||||
|
// We also strip super because its not used for translation
|
||||||
|
// on macos and it results in a bad translation.
|
||||||
|
if (comptime builtin.target.isDarwin()) {
|
||||||
|
translate_mods.ctrl = false;
|
||||||
|
translate_mods.super = false;
|
||||||
|
}
|
||||||
|
|
||||||
break :translate_mods translate_mods;
|
break :translate_mods translate_mods;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Translate our key using the keymap for our localized keyboard layout.
|
// Translate our key using the keymap for our localized keyboard layout.
|
||||||
|
// We only translate for keydown events. Otherwise, we only care about
|
||||||
|
// the raw keycode.
|
||||||
var buf: [128]u8 = undefined;
|
var buf: [128]u8 = undefined;
|
||||||
const result = try self.app.keymap.translate(
|
const result: input.Keymap.Translation = if (is_down) translate: {
|
||||||
&buf,
|
const result = try self.app.keymap.translate(
|
||||||
&self.keymap_state,
|
&buf,
|
||||||
@intCast(keycode),
|
&self.keymap_state,
|
||||||
translate_mods,
|
@intCast(keycode),
|
||||||
);
|
translate_mods,
|
||||||
|
);
|
||||||
|
|
||||||
// If we aren't composing, then we set our preedit to empty no matter what.
|
// If this is a dead key, then we're composing a character and
|
||||||
if (!result.composing) {
|
// we need to set our proper preedit state.
|
||||||
self.core_surface.preeditCallback(null) catch {};
|
if (result.composing) {
|
||||||
}
|
const view = std.unicode.Utf8View.init(result.text) catch |err| {
|
||||||
|
log.warn("cannot build utf8 view over input: {}", .{err});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
var it = view.iterator();
|
||||||
|
|
||||||
|
const cp: u21 = it.nextCodepoint() orelse 0;
|
||||||
|
self.core_surface.preeditCallback(cp) catch |err| {
|
||||||
|
log.err("error in preedit callback err={}", .{err});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// If we aren't composing, then we set our preedit to
|
||||||
|
// empty no matter what.
|
||||||
|
self.core_surface.preeditCallback(null) catch {};
|
||||||
|
}
|
||||||
|
|
||||||
|
break :translate result;
|
||||||
|
} else .{ .composing = false, .text = "" };
|
||||||
|
|
||||||
|
// UCKeyTranslate always consumes all mods, so if we have any output
|
||||||
|
// then we've consumed our translate mods.
|
||||||
|
const consumed_mods: input.Mods = if (result.text.len > 0) translate_mods else .{};
|
||||||
|
|
||||||
// log.warn("TRANSLATE: action={} keycode={x} dead={} key_len={} key={any} key_str={s} mods={}", .{
|
// log.warn("TRANSLATE: action={} keycode={x} dead={} key_len={} key={any} key_str={s} mods={}", .{
|
||||||
// action,
|
// action,
|
||||||
@ -443,12 +479,14 @@ pub const Surface = struct {
|
|||||||
// charCallback.
|
// charCallback.
|
||||||
//
|
//
|
||||||
// We also only do key translation if this is not a dead key.
|
// We also only do key translation if this is not a dead key.
|
||||||
const key = if (!result.composing and result.text.len == 1) key: {
|
const key = if (!result.composing) key: {
|
||||||
// A completed key. If the length of the key is one then we can
|
// A completed key. If the length of the key is one then we can
|
||||||
// attempt to translate it to a key enum and call the key
|
// attempt to translate it to a key enum and call the key
|
||||||
// callback. First try plain ASCII.
|
// callback. First try plain ASCII.
|
||||||
if (input.Key.fromASCII(result.text[0])) |key| {
|
if (result.text.len > 0) {
|
||||||
break :key key;
|
if (input.Key.fromASCII(result.text[0])) |key| {
|
||||||
|
break :key key;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If that doesn't work then we try to translate without
|
// If that doesn't work then we try to translate without
|
||||||
@ -470,59 +508,49 @@ pub const Surface = struct {
|
|||||||
break :key physical_key;
|
break :key physical_key;
|
||||||
} else .invalid;
|
} else .invalid;
|
||||||
|
|
||||||
// If both keys are invalid then we won't call the key callback. But
|
// Invoke the core Ghostty logic to handle this input.
|
||||||
// if either one is valid, we want to give it a chance.
|
const consumed = self.core_surface.keyCallback(.{
|
||||||
if (key != .invalid or physical_key != .invalid) {
|
.action = action,
|
||||||
const consumed = self.core_surface.keyCallback(
|
.key = key,
|
||||||
action,
|
.physical_key = physical_key,
|
||||||
key,
|
.mods = mods,
|
||||||
physical_key,
|
.consumed_mods = consumed_mods,
|
||||||
mods,
|
.composing = result.composing,
|
||||||
) catch |err| {
|
.utf8 = result.text,
|
||||||
log.err("error in key callback err={}", .{err});
|
}) catch |err| {
|
||||||
return;
|
log.err("error in key callback err={}", .{err});
|
||||||
};
|
|
||||||
|
|
||||||
// If we consume the key then we want to reset the dead key state.
|
|
||||||
if (consumed) {
|
|
||||||
self.keymap_state = .{};
|
|
||||||
self.core_surface.preeditCallback(null) catch {};
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// No matter what happens next we'll want a utf8 view.
|
|
||||||
const view = std.unicode.Utf8View.init(result.text) catch |err| {
|
|
||||||
log.warn("cannot build utf8 view over input: {}", .{err});
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
var it = view.iterator();
|
|
||||||
|
|
||||||
// If this is a dead key, then we're composing a character and
|
|
||||||
// we end processing here. We don't process keybinds for dead keys.
|
|
||||||
if (result.composing) {
|
|
||||||
const cp: u21 = it.nextCodepoint() orelse 0;
|
|
||||||
self.core_surface.preeditCallback(cp) catch |err| {
|
|
||||||
log.err("error in preedit callback err={}", .{err});
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// If we consume the key then we want to reset the dead key state.
|
||||||
|
if (consumed and is_down) {
|
||||||
|
self.keymap_state = .{};
|
||||||
|
self.core_surface.preeditCallback(null) catch {};
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next, we want to call the char callback with each codepoint.
|
|
||||||
while (it.nextCodepoint()) |cp| {
|
|
||||||
self.core_surface.charCallback(cp, mods) catch |err| {
|
|
||||||
log.err("error in char callback err={}", .{err});
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn charCallback(self: *Surface, cp_: u32) void {
|
pub fn charCallback(self: *Surface, cp_: u32) void {
|
||||||
const cp = std.math.cast(u21, cp_) orelse return;
|
const cp = std.math.cast(u21, cp_) orelse return;
|
||||||
self.core_surface.charCallback(cp, .{}) catch |err| {
|
var buf: [4]u8 = undefined;
|
||||||
log.err("error in char callback err={}", .{err});
|
const len = std.unicode.utf8Encode(cp, &buf) catch |err| {
|
||||||
|
log.err("error encoding codepoint={} err={}", .{ cp, err });
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// For a char callback we just construct a key event with invalid
|
||||||
|
// keys but with text. This should result in the text being sent
|
||||||
|
// as-is.
|
||||||
|
_ = self.core_surface.keyCallback(.{
|
||||||
|
.action = .press,
|
||||||
|
.key = .invalid,
|
||||||
|
.physical_key = .invalid,
|
||||||
|
.mods = .{},
|
||||||
|
.consumed_mods = .{},
|
||||||
|
.composing = false,
|
||||||
|
.utf8 = buf[0..len],
|
||||||
|
}) catch |err| {
|
||||||
|
log.err("error in key callback err={}", .{err});
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -282,10 +282,11 @@ pub const Surface = struct {
|
|||||||
/// A core surface
|
/// A core surface
|
||||||
core_surface: CoreSurface,
|
core_surface: CoreSurface,
|
||||||
|
|
||||||
/// This is set to true when keyCallback consumes the input, suppressing
|
/// This is the key event that was processed in keyCallback. This is only
|
||||||
/// the charCallback from being fired.
|
/// non-null if the event was NOT consumed in keyCallback. This lets us
|
||||||
key_consumed: bool = false,
|
/// know in charCallback whether we should populate it and call it again.
|
||||||
key_mods: input.Mods = .{},
|
/// (GLFW guarantees that charCallback is called after keyCallback).
|
||||||
|
key_event: ?input.KeyEvent = null,
|
||||||
|
|
||||||
pub const Options = struct {};
|
pub const Options = struct {};
|
||||||
|
|
||||||
@ -592,14 +593,21 @@ pub const Surface = struct {
|
|||||||
|
|
||||||
const core_win = window.getUserPointer(CoreSurface) orelse return;
|
const core_win = window.getUserPointer(CoreSurface) orelse return;
|
||||||
|
|
||||||
// If our keyCallback consumed the key input, don't emit a char.
|
// We need a key event in order to process the charcallback. If it
|
||||||
if (core_win.rt_surface.key_consumed) {
|
// isn't set then the key event was consumed.
|
||||||
core_win.rt_surface.key_consumed = false;
|
var key_event = core_win.rt_surface.key_event orelse return;
|
||||||
return;
|
core_win.rt_surface.key_event = null;
|
||||||
}
|
|
||||||
|
|
||||||
core_win.charCallback(codepoint, core_win.rt_surface.key_mods) catch |err| {
|
// Populate the utf8 value for the event
|
||||||
log.err("error in char callback err={}", .{err});
|
var buf: [4]u8 = undefined;
|
||||||
|
const len = std.unicode.utf8Encode(codepoint, &buf) catch |err| {
|
||||||
|
log.err("error encoding codepoint={} err={}", .{ codepoint, err });
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
key_event.utf8 = buf[0..len];
|
||||||
|
|
||||||
|
_ = core_win.keyCallback(key_event) catch |err| {
|
||||||
|
log.err("error in key callback err={}", .{err});
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -755,18 +763,28 @@ pub const Surface = struct {
|
|||||||
=> .invalid,
|
=> .invalid,
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: we need to do mapped keybindings
|
const key_event: input.KeyEvent = .{
|
||||||
|
.action = action,
|
||||||
|
.key = key,
|
||||||
|
.physical_key = key,
|
||||||
|
.mods = mods,
|
||||||
|
.consumed_mods = .{},
|
||||||
|
.composing = false,
|
||||||
|
.utf8 = "",
|
||||||
|
};
|
||||||
|
|
||||||
core_win.rt_surface.key_mods = mods;
|
const consumed = core_win.keyCallback(key_event) catch |err| {
|
||||||
core_win.rt_surface.key_consumed = 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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// If it wasn't consumed, we set it on our self so that charcallback
|
||||||
|
// can make another attempt. Otherwise, we set null so the charcallback
|
||||||
|
// is ignored.
|
||||||
|
core_win.rt_surface.key_event = null;
|
||||||
|
if (!consumed and (action == .press or action == .repeat)) {
|
||||||
|
core_win.rt_surface.key_event = key_event;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn focusCallback(window: glfw.Window, focused: bool) void {
|
fn focusCallback(window: glfw.Window, focused: bool) void {
|
||||||
|
@ -1155,33 +1155,6 @@ pub const Surface = struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Key press event. This is where we do ALL of our key handling,
|
|
||||||
/// translation to keyboard layouts, dead key handling, etc. Key handling
|
|
||||||
/// is complicated so this comment will explain what's going on.
|
|
||||||
///
|
|
||||||
/// At a high level, we want to do the following:
|
|
||||||
///
|
|
||||||
/// 1. Emit a keyCallback for the key press with the right keys.
|
|
||||||
/// 2. Emit a charCallback if a unicode char was generated from the
|
|
||||||
/// keypresses, but only if keyCallback didn't consume the input.
|
|
||||||
///
|
|
||||||
/// This callback will first set the "in_keypress" flag to true. This
|
|
||||||
/// lets our IM callbacks know that we're in a keypress event so they don't
|
|
||||||
/// emit a charCallback since this function will do it after the keyCallback
|
|
||||||
/// (remember, the order matters!).
|
|
||||||
///
|
|
||||||
/// Next, we run the keypress through the input method context in order
|
|
||||||
/// to determine if we're in a dead key state, completed unicode char, etc.
|
|
||||||
/// This all happens through various callbacks: preedit, commit, etc.
|
|
||||||
/// These inspect "in_keypress" if they have to and set some instance
|
|
||||||
/// state.
|
|
||||||
///
|
|
||||||
/// Finally, we map our keys to input.Keys, emit the keyCallback, then
|
|
||||||
/// emit the charCallback if we have to.
|
|
||||||
///
|
|
||||||
/// Note we ALSO have an IMContext attached directly to the widget
|
|
||||||
/// which can emit preedit and commit callbacks. But, if we're not
|
|
||||||
/// in a keypress, we let those automatically work.
|
|
||||||
fn gtkKeyPressed(
|
fn gtkKeyPressed(
|
||||||
ec_key: *c.GtkEventControllerKey,
|
ec_key: *c.GtkEventControllerKey,
|
||||||
keyval: c.guint,
|
keyval: c.guint,
|
||||||
@ -1189,34 +1162,106 @@ pub const Surface = struct {
|
|||||||
gtk_mods: c.GdkModifierType,
|
gtk_mods: c.GdkModifierType,
|
||||||
ud: ?*anyopaque,
|
ud: ?*anyopaque,
|
||||||
) callconv(.C) c.gboolean {
|
) callconv(.C) c.gboolean {
|
||||||
|
return if (keyEvent(.press, ec_key, keyval, keycode, gtk_mods, ud)) 1 else 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gtkKeyReleased(
|
||||||
|
ec_key: *c.GtkEventControllerKey,
|
||||||
|
keyval: c.guint,
|
||||||
|
keycode: c.guint,
|
||||||
|
state: c.GdkModifierType,
|
||||||
|
ud: ?*anyopaque,
|
||||||
|
) callconv(.C) c.gboolean {
|
||||||
|
return if (keyEvent(.release, ec_key, keyval, keycode, state, ud)) 1 else 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Key press event. This is where we do ALL of our key handling,
|
||||||
|
/// translation to keyboard layouts, dead key handling, etc. Key handling
|
||||||
|
/// is complicated so this comment will explain what's going on.
|
||||||
|
///
|
||||||
|
/// At a high level, we want to construct an `input.KeyEvent` and
|
||||||
|
/// pass that to `keyCallback`. At a low level, this is more complicated
|
||||||
|
/// than it appears because we need to construct all of this information
|
||||||
|
/// and its not given to us.
|
||||||
|
///
|
||||||
|
/// For press events, we run the keypress through the input method context
|
||||||
|
/// in order to determine if we're in a dead key state, completed unicode
|
||||||
|
/// char, etc. This all happens through various callbacks: preedit, commit,
|
||||||
|
/// etc. These inspect "in_keypress" if they have to and set some instance
|
||||||
|
/// state.
|
||||||
|
///
|
||||||
|
/// We then take all of the information in order to determine if we have
|
||||||
|
/// a unicode character or if we have to map the keyval to a code to
|
||||||
|
/// get the underlying logical key, etc.
|
||||||
|
///
|
||||||
|
/// Finally, we can emit the keyCallback.
|
||||||
|
///
|
||||||
|
/// Note we ALSO have an IMContext attached directly to the widget
|
||||||
|
/// which can emit preedit and commit callbacks. But, if we're not
|
||||||
|
/// in a keypress, we let those automatically work.
|
||||||
|
fn keyEvent(
|
||||||
|
action: input.Action,
|
||||||
|
ec_key: *c.GtkEventControllerKey,
|
||||||
|
keyval: c.guint,
|
||||||
|
keycode: c.guint,
|
||||||
|
gtk_mods: c.GdkModifierType,
|
||||||
|
ud: ?*anyopaque,
|
||||||
|
) bool {
|
||||||
const self = userdataSelf(ud.?);
|
const self = userdataSelf(ud.?);
|
||||||
const mods = translateMods(gtk_mods);
|
const mods = translateMods(gtk_mods);
|
||||||
|
const keyval_unicode = c.gdk_keyval_to_unicode(keyval);
|
||||||
// We mark that we're in a keypress event. We use this in our
|
const event = c.gtk_event_controller_get_current_event(@ptrCast(ec_key));
|
||||||
// IM commit callback to determine if we need to send a char callback
|
|
||||||
// to the core surface or not.
|
|
||||||
self.in_keypress = true;
|
|
||||||
defer self.in_keypress = false;
|
|
||||||
|
|
||||||
// We always reset our committed text when ending a keypress so that
|
// We always reset our committed text when ending a keypress so that
|
||||||
// future keypresses don't think we have a commit event.
|
// future keypresses don't think we have a commit event.
|
||||||
defer self.im_len = 0;
|
defer self.im_len = 0;
|
||||||
|
|
||||||
|
// We only want to send the event through the IM context if we're a press
|
||||||
|
if (action == .press or action == .repeat) {
|
||||||
|
// We mark that we're in a keypress event. We use this in our
|
||||||
|
// IM commit callback to determine if we need to send a char callback
|
||||||
|
// to the core surface or not.
|
||||||
|
self.in_keypress = true;
|
||||||
|
defer self.in_keypress = false;
|
||||||
|
|
||||||
|
// Pass the event through the IM controller to handle dead key states.
|
||||||
|
// Filter is true if the event was handled by the IM controller.
|
||||||
|
_ = c.gtk_im_context_filter_keypress(self.im_context, event) != 0;
|
||||||
|
|
||||||
|
// If this is a dead key, then we're composing a character and
|
||||||
|
// we need to set our proper preedit state.
|
||||||
|
if (self.im_composing) preedit: {
|
||||||
|
const text = self.im_buf[0..self.im_len];
|
||||||
|
const view = std.unicode.Utf8View.init(text) catch |err| {
|
||||||
|
log.warn("cannot build utf8 view over input: {}", .{err});
|
||||||
|
break :preedit;
|
||||||
|
};
|
||||||
|
var it = view.iterator();
|
||||||
|
|
||||||
|
const cp: u21 = it.nextCodepoint() orelse 0;
|
||||||
|
self.core_surface.preeditCallback(cp) catch |err| {
|
||||||
|
log.err("error in preedit callback err={}", .{err});
|
||||||
|
break :preedit;
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// If we aren't composing, then we set our preedit to
|
||||||
|
// empty no matter what.
|
||||||
|
self.core_surface.preeditCallback(null) catch {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// We want to get the physical unmapped key to process physical keybinds.
|
// We want to get the physical unmapped key to process physical keybinds.
|
||||||
// (These are keybinds explicitly marked as requesting physical mapping).
|
// (These are keybinds explicitly marked as requesting physical mapping).
|
||||||
const physical_key = keycode: for (input.keycodes.entries) |entry| {
|
const physical_key = keycode: for (input.keycodes.entries) |entry| {
|
||||||
if (entry.native == keycode) break :keycode entry.key;
|
if (entry.native == keycode) break :keycode entry.key;
|
||||||
} else .invalid;
|
} else .invalid;
|
||||||
|
|
||||||
// Pass the event through the IM controller to handle dead key states.
|
// Get our consumed modifiers
|
||||||
// Filter is true if the event was handled by the IM controller.
|
const consumed_mods: input.Mods = consumed: {
|
||||||
const event = c.gtk_event_controller_get_current_event(@ptrCast(ec_key));
|
const raw = c.gdk_key_event_get_consumed_modifiers(event);
|
||||||
_ = c.gtk_im_context_filter_keypress(self.im_context, event) != 0;
|
const masked = raw & c.GDK_MODIFIER_MASK;
|
||||||
|
break :consumed translateMods(masked);
|
||||||
// If we aren't composing, then we set our preedit to empty no matter what.
|
};
|
||||||
if (!self.im_composing) {
|
|
||||||
self.core_surface.preeditCallback(null) catch {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we're not in a dead key state, we want to translate our text
|
// If we're not in a dead key state, we want to translate our text
|
||||||
// to some input.Key.
|
// to some input.Key.
|
||||||
@ -1231,7 +1276,6 @@ pub const Surface = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If that doesn't work then we try to translate they kevval..
|
// If that doesn't work then we try to translate they kevval..
|
||||||
const keyval_unicode = c.gdk_keyval_to_unicode(keyval);
|
|
||||||
if (keyval_unicode != 0) {
|
if (keyval_unicode != 0) {
|
||||||
if (std.math.cast(u8, keyval_unicode)) |byte| {
|
if (std.math.cast(u8, keyval_unicode)) |byte| {
|
||||||
if (input.Key.fromASCII(byte)) |key| {
|
if (input.Key.fromASCII(byte)) |key| {
|
||||||
@ -1252,100 +1296,40 @@ pub const Surface = struct {
|
|||||||
// mods,
|
// mods,
|
||||||
// });
|
// });
|
||||||
|
|
||||||
// If both keys are invalid then we won't call the key callback. But
|
// If we have no UTF-8 text, we try to convert our keyval to
|
||||||
// if either one is valid, we want to give it a chance.
|
// a text value. We have to do this because GTK will not process
|
||||||
if (key != .invalid or physical_key != .invalid) {
|
|
||||||
const consumed = self.core_surface.keyCallback(
|
|
||||||
.press,
|
|
||||||
key,
|
|
||||||
physical_key,
|
|
||||||
mods,
|
|
||||||
) catch |err| {
|
|
||||||
log.err("error in key callback err={}", .{err});
|
|
||||||
return 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
// If we consume the key then we want to reset the dead key state.
|
|
||||||
if (consumed) {
|
|
||||||
c.gtk_im_context_reset(self.im_context);
|
|
||||||
self.core_surface.preeditCallback(null) catch {};
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If this is a dead key, then we're composing a character and
|
|
||||||
// we end processing here. We don't process keybinds for dead keys.
|
|
||||||
if (self.im_composing) {
|
|
||||||
const text = self.im_buf[0..self.im_len];
|
|
||||||
const view = std.unicode.Utf8View.init(text) catch |err| {
|
|
||||||
log.warn("cannot build utf8 view over input: {}", .{err});
|
|
||||||
return 0;
|
|
||||||
};
|
|
||||||
var it = view.iterator();
|
|
||||||
|
|
||||||
const cp: u21 = it.nextCodepoint() orelse 0;
|
|
||||||
self.core_surface.preeditCallback(cp) catch |err| {
|
|
||||||
log.err("error in preedit callback err={}", .{err});
|
|
||||||
return 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we aren't composing and have no text, we try to convert the keyval
|
|
||||||
// to a text value. We have to do this because GTK will not process
|
|
||||||
// "Ctrl+Shift+1" (on US keyboards) as "Ctrl+!" but instead as "".
|
// "Ctrl+Shift+1" (on US keyboards) as "Ctrl+!" but instead as "".
|
||||||
// But the keyval is set correctly so we can at least extract that.
|
// But the keyval is set correctly so we can at least extract that.
|
||||||
if (self.im_len == 0) {
|
if (self.im_len == 0 and keyval_unicode > 0) {
|
||||||
const keyval_unicode = c.gdk_keyval_to_unicode(keyval);
|
if (std.math.cast(u21, keyval_unicode)) |cp| {
|
||||||
if (keyval_unicode != 0) {
|
if (std.unicode.utf8Encode(cp, &self.im_buf)) |len| {
|
||||||
if (std.math.cast(u21, keyval_unicode)) |cp| {
|
self.im_len = len;
|
||||||
if (std.unicode.utf8Encode(cp, &self.im_buf)) |len| {
|
} else |_| {}
|
||||||
self.im_len = len;
|
|
||||||
} else |_| {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next, we want to call the char callback with each codepoint.
|
// Invoke the core Ghostty logic to handle this input.
|
||||||
if (self.im_len > 0) {
|
const consumed = self.core_surface.keyCallback(.{
|
||||||
const text = self.im_buf[0..self.im_len];
|
.action = action,
|
||||||
const view = std.unicode.Utf8View.init(text) catch |err| {
|
.key = key,
|
||||||
log.warn("cannot build utf8 view over input: {}", .{err});
|
.physical_key = physical_key,
|
||||||
return 0;
|
.mods = mods,
|
||||||
};
|
.consumed_mods = consumed_mods,
|
||||||
var it = view.iterator();
|
.composing = self.im_composing,
|
||||||
while (it.nextCodepoint()) |cp| {
|
.utf8 = self.im_buf[0..self.im_len],
|
||||||
self.core_surface.charCallback(cp, mods) catch |err| {
|
}) catch |err| {
|
||||||
log.err("error in char callback err={}", .{err});
|
|
||||||
return 0;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn gtkKeyReleased(
|
|
||||||
_: *c.GtkEventControllerKey,
|
|
||||||
keyval: c.guint,
|
|
||||||
keycode: c.guint,
|
|
||||||
state: c.GdkModifierType,
|
|
||||||
ud: ?*anyopaque,
|
|
||||||
) callconv(.C) c.gboolean {
|
|
||||||
_ = keycode;
|
|
||||||
|
|
||||||
const key = translateKey(keyval);
|
|
||||||
const mods = translateMods(state);
|
|
||||||
const self = userdataSelf(ud.?);
|
|
||||||
const consumed = 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 false;
|
||||||
};
|
};
|
||||||
|
|
||||||
return if (consumed) 1 else 0;
|
// If we consume the key then we want to reset the dead key state.
|
||||||
|
if (consumed and (action == .press or action == .repeat)) {
|
||||||
|
c.gtk_im_context_reset(self.im_context);
|
||||||
|
self.core_surface.preeditCallback(null) catch {};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gtkInputPreeditStart(
|
fn gtkInputPreeditStart(
|
||||||
@ -1418,17 +1402,18 @@ pub const Surface = struct {
|
|||||||
// We're not in a keypress, so this was sent from an on-screen emoji
|
// We're not in a keypress, so this was sent from an on-screen emoji
|
||||||
// keyboard or someting like that. Send the characters directly to
|
// keyboard or someting like that. Send the characters directly to
|
||||||
// the surface.
|
// the surface.
|
||||||
const view = std.unicode.Utf8View.init(str) catch |err| {
|
_ = self.core_surface.keyCallback(.{
|
||||||
log.warn("cannot build utf8 view over input: {}", .{err});
|
.action = .press,
|
||||||
|
.key = .invalid,
|
||||||
|
.physical_key = .invalid,
|
||||||
|
.mods = .{},
|
||||||
|
.consumed_mods = .{},
|
||||||
|
.composing = false,
|
||||||
|
.utf8 = str,
|
||||||
|
}) catch |err| {
|
||||||
|
log.err("error in key callback err={}", .{err});
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
var it = view.iterator();
|
|
||||||
while (it.nextCodepoint()) |cp| {
|
|
||||||
self.core_surface.charCallback(cp, .{}) catch |err| {
|
|
||||||
log.err("error in char callback err={}", .{err});
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gtkFocusEnter(_: *c.GtkEventControllerFocus, ud: ?*anyopaque) callconv(.C) void {
|
fn gtkFocusEnter(_: *c.GtkEventControllerFocus, ud: ?*anyopaque) callconv(.C) void {
|
||||||
|
@ -6,6 +6,7 @@ pub usingnamespace @import("input/key.zig");
|
|||||||
pub const function_keys = @import("input/function_keys.zig");
|
pub const function_keys = @import("input/function_keys.zig");
|
||||||
pub const keycodes = @import("input/keycodes.zig");
|
pub const keycodes = @import("input/keycodes.zig");
|
||||||
pub const Binding = @import("input/Binding.zig");
|
pub const Binding = @import("input/Binding.zig");
|
||||||
|
pub const KeyEncoder = @import("input/KeyEncoder.zig");
|
||||||
pub const SplitDirection = Binding.Action.SplitDirection;
|
pub const SplitDirection = Binding.Action.SplitDirection;
|
||||||
pub const SplitFocusDirection = Binding.Action.SplitFocusDirection;
|
pub const SplitFocusDirection = Binding.Action.SplitFocusDirection;
|
||||||
|
|
||||||
|
471
src/input/KeyEncoder.zig
Normal file
471
src/input/KeyEncoder.zig
Normal file
@ -0,0 +1,471 @@
|
|||||||
|
/// KeyEncoder is responsible for processing keyboard input and generating
|
||||||
|
/// the proper VT sequence for any events.
|
||||||
|
///
|
||||||
|
/// A new KeyEncoder should be created for each individual key press.
|
||||||
|
/// These encoders are not meant to be reused.
|
||||||
|
const KeyEncoder = @This();
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
const key = @import("key.zig");
|
||||||
|
const function_keys = @import("function_keys.zig");
|
||||||
|
|
||||||
|
event: key.KeyEvent,
|
||||||
|
|
||||||
|
/// The state of various modes of a terminal that impact encoding.
|
||||||
|
alt_esc_prefix: bool = false,
|
||||||
|
cursor_key_application: bool = false,
|
||||||
|
keypad_key_application: bool = false,
|
||||||
|
modify_other_keys_state_2: bool = false,
|
||||||
|
|
||||||
|
/// Perform legacy encoding of the key event. "Legacy" in this case
|
||||||
|
/// is referring to the behavior of traditional terminals, plus
|
||||||
|
/// xterm's `modifyOtherKeys`, plus Paul Evans's "fixterms" spec.
|
||||||
|
/// These together combine the legacy protocol because they're all
|
||||||
|
/// meant to be extensions that do not change any existing behavior
|
||||||
|
/// and therefore safe to combine.
|
||||||
|
pub fn legacy(
|
||||||
|
self: *const KeyEncoder,
|
||||||
|
buf: []u8,
|
||||||
|
) ![]const u8 {
|
||||||
|
const all_mods = self.event.mods;
|
||||||
|
const effective_mods = self.event.effectiveMods();
|
||||||
|
const binding_mods = effective_mods.binding();
|
||||||
|
|
||||||
|
// Legacy encoding only does press/repeat
|
||||||
|
if (self.event.action != .press and
|
||||||
|
self.event.action != .repeat) return "";
|
||||||
|
|
||||||
|
// If we're in a dead key state then we never emit a sequence.
|
||||||
|
if (self.event.composing) return "";
|
||||||
|
|
||||||
|
// If we match a PC style function key then that is our result.
|
||||||
|
if (pcStyleFunctionKey(
|
||||||
|
self.event.key,
|
||||||
|
binding_mods,
|
||||||
|
self.cursor_key_application,
|
||||||
|
self.keypad_key_application,
|
||||||
|
self.modify_other_keys_state_2,
|
||||||
|
)) |sequence| return copyToBuf(buf, sequence);
|
||||||
|
|
||||||
|
// If we match a control sequence, we output that directly. For
|
||||||
|
// ctrlSeq we have to use all mods because we want it to only
|
||||||
|
// match ctrl+<char>.
|
||||||
|
if (ctrlSeq(self.event.key, all_mods)) |char| {
|
||||||
|
// C0 sequences support alt-as-esc prefixing.
|
||||||
|
if (binding_mods.alt) {
|
||||||
|
if (buf.len < 2) return error.OutOfMemory;
|
||||||
|
buf[0] = 0x1B;
|
||||||
|
buf[1] = char;
|
||||||
|
return buf[0..2];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buf.len < 1) return error.OutOfMemory;
|
||||||
|
buf[0] = char;
|
||||||
|
return buf[0..1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have no UTF8 text then at this point there is nothing to do.
|
||||||
|
const utf8 = self.event.utf8;
|
||||||
|
if (utf8.len == 0) return "";
|
||||||
|
|
||||||
|
// In modify other keys state 2, we send the CSI 27 sequence
|
||||||
|
// for any char with a modifier. Ctrl sequences like Ctrl+a
|
||||||
|
// are already handled above.
|
||||||
|
if (self.modify_other_keys_state_2) modify_other: {
|
||||||
|
const view = try std.unicode.Utf8View.init(utf8);
|
||||||
|
var it = view.iterator();
|
||||||
|
const codepoint = it.nextCodepoint() orelse break :modify_other;
|
||||||
|
|
||||||
|
// We only do this if we have a single codepoint. There shouldn't
|
||||||
|
// ever be a multi-codepoint sequence that triggers this.
|
||||||
|
if (it.nextCodepoint() != null) break :modify_other;
|
||||||
|
|
||||||
|
// This copies xterm's `ModifyOtherKeys` function that returns
|
||||||
|
// whether modify other keys should be encoded for the given
|
||||||
|
// input.
|
||||||
|
const should_modify = should_modify: {
|
||||||
|
// xterm IsControlInput
|
||||||
|
if (codepoint >= 0x40 and codepoint <= 0x7F)
|
||||||
|
break :should_modify true;
|
||||||
|
|
||||||
|
// If we have anything other than shift pressed, encode.
|
||||||
|
var mods_no_shift = binding_mods;
|
||||||
|
mods_no_shift.shift = false;
|
||||||
|
if (!mods_no_shift.empty()) break :should_modify true;
|
||||||
|
|
||||||
|
// We only have shift pressed. We only allow space.
|
||||||
|
if (codepoint == ' ') break :should_modify true;
|
||||||
|
|
||||||
|
// This logic isn't complete but I don't fully understand
|
||||||
|
// the rest so I'm going to wait until we can have a
|
||||||
|
// reasonable test scenario.
|
||||||
|
break :should_modify false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (should_modify) {
|
||||||
|
for (function_keys.modifiers, 2..) |modset, code| {
|
||||||
|
if (!binding_mods.equal(modset)) continue;
|
||||||
|
return try std.fmt.bufPrint(
|
||||||
|
buf,
|
||||||
|
"\x1B[27;{};{}~",
|
||||||
|
.{ code, codepoint },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let's see if we should apply fixterms to this codepoint.
|
||||||
|
// At this stage of key processing, we only need to apply fixterms
|
||||||
|
// to unicode codepoints if we have ctrl set.
|
||||||
|
if (self.event.mods.ctrl) {
|
||||||
|
// Important: we want to use the original mods here, not the
|
||||||
|
// effective mods. The fixterms spec states the shifted chars
|
||||||
|
// should be sent uppercase but Kitty changes that behavior
|
||||||
|
// so we'll send all the mods.
|
||||||
|
const csi_u_mods = CsiUMods.fromInput(self.event.mods);
|
||||||
|
const result = try std.fmt.bufPrint(
|
||||||
|
buf,
|
||||||
|
"\x1B[{};{}u",
|
||||||
|
.{ utf8[0], csi_u_mods.seqInt() },
|
||||||
|
);
|
||||||
|
// std.log.warn("CSI_U: {s}", .{result});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have alt-pressed and alt-esc-prefix is enabled, then
|
||||||
|
// we need to prefix the utf8 sequence with an esc.
|
||||||
|
if (binding_mods.alt and self.alt_esc_prefix) {
|
||||||
|
// TODO: port this, I think we can just use effective mods
|
||||||
|
// without any OS special case
|
||||||
|
//
|
||||||
|
// On macOS, we have to opt-in to using alt because option
|
||||||
|
// by default is a unicode character sequence.
|
||||||
|
// if (comptime builtin.target.isDarwin()) {
|
||||||
|
// switch (self.config.macos_option_as_alt) {
|
||||||
|
// .false => break :alt,
|
||||||
|
// .true => {},
|
||||||
|
// .left => if (mods.sides.alt != .left) break :alt,
|
||||||
|
// .right => if (mods.sides.alt != .right) break :alt,
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
return try std.fmt.bufPrint(buf, "\x1B{s}", .{utf8});
|
||||||
|
}
|
||||||
|
|
||||||
|
return try copyToBuf(buf, utf8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A helper to memcpy a src value to a buffer and return the result.
|
||||||
|
fn copyToBuf(buf: []u8, src: []const u8) ![]const u8 {
|
||||||
|
if (src.len > buf.len) return error.OutOfMemory;
|
||||||
|
const result = buf[0..src.len];
|
||||||
|
@memcpy(result, src);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determines whether the key should be encoded in the xterm
|
||||||
|
/// "PC-style Function Key" syntax (roughly). This is a hardcoded
|
||||||
|
/// table of keys and modifiers that result in a specific sequence.
|
||||||
|
fn pcStyleFunctionKey(
|
||||||
|
keyval: key.Key,
|
||||||
|
mods: key.Mods,
|
||||||
|
cursor_key_application: bool,
|
||||||
|
keypad_key_application: bool,
|
||||||
|
modify_other_keys: bool, // True if state 2
|
||||||
|
) ?[]const u8 {
|
||||||
|
const mods_int = mods.int();
|
||||||
|
for (function_keys.keys.get(keyval)) |entry| {
|
||||||
|
switch (entry.cursor) {
|
||||||
|
.any => {},
|
||||||
|
.normal => if (cursor_key_application) continue,
|
||||||
|
.application => if (!cursor_key_application) continue,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (entry.keypad) {
|
||||||
|
.any => {},
|
||||||
|
.normal => if (keypad_key_application) continue,
|
||||||
|
.application => if (!keypad_key_application) continue,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (entry.modify_other_keys) {
|
||||||
|
.any => {},
|
||||||
|
.set => if (modify_other_keys) continue,
|
||||||
|
.set_other => if (!modify_other_keys) continue,
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry_mods_int = entry.mods.int();
|
||||||
|
if (entry_mods_int == 0) {
|
||||||
|
if (mods_int != 0 and !entry.mods_empty_is_any) continue;
|
||||||
|
// mods are either empty, or empty means any so we allow it.
|
||||||
|
} else if (entry_mods_int != mods_int) {
|
||||||
|
// any set mods require an exact match
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry.sequence;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the C0 byte for the key event if it should be used.
|
||||||
|
/// This converts a key event into the expected terminal behavior
|
||||||
|
/// such as Ctrl+C turning into 0x03, amongst many other translations.
|
||||||
|
///
|
||||||
|
/// This will return null if the key event should not be converted
|
||||||
|
/// into a C0 byte. There are many cases for this and you should read
|
||||||
|
/// the source code to understand them.
|
||||||
|
fn ctrlSeq(keyval: key.Key, mods: key.Mods) ?u8 {
|
||||||
|
// Remove alt from our modifiers because it does not impact whether
|
||||||
|
// we are generating a ctrl sequence.
|
||||||
|
const unalt_mods = unalt_mods: {
|
||||||
|
var unalt_mods = mods;
|
||||||
|
unalt_mods.alt = false;
|
||||||
|
break :unalt_mods unalt_mods.binding();
|
||||||
|
};
|
||||||
|
|
||||||
|
// If we have any other modifier key set, then we do not generate
|
||||||
|
// a C0 sequence.
|
||||||
|
const ctrl_only = comptime (key.Mods{ .ctrl = true }).int();
|
||||||
|
if (unalt_mods.int() != ctrl_only) return null;
|
||||||
|
|
||||||
|
// The normal approach to get this value is to make the ascii byte
|
||||||
|
// with 0x1F. However, not all apprt key translation will properly
|
||||||
|
// generate the correct value so we just hardcode this based on
|
||||||
|
// logical key.
|
||||||
|
return switch (keyval) {
|
||||||
|
.space => 0,
|
||||||
|
.slash => 0x1F,
|
||||||
|
.zero => 0x30,
|
||||||
|
.one => 0x31,
|
||||||
|
.two => 0x00,
|
||||||
|
.three => 0x1B,
|
||||||
|
.four => 0x1C,
|
||||||
|
.five => 0x1D,
|
||||||
|
.six => 0x1E,
|
||||||
|
.seven => 0x1F,
|
||||||
|
.eight => 0x7F,
|
||||||
|
.nine => 0x39,
|
||||||
|
.backslash => 0x1C,
|
||||||
|
.right_bracket => 0x1D,
|
||||||
|
.a => 0x01,
|
||||||
|
.b => 0x02,
|
||||||
|
.c => 0x03,
|
||||||
|
.d => 0x04,
|
||||||
|
.e => 0x05,
|
||||||
|
.f => 0x06,
|
||||||
|
.g => 0x07,
|
||||||
|
.h => 0x08,
|
||||||
|
.j => 0x0A,
|
||||||
|
.k => 0x0B,
|
||||||
|
.l => 0x0C,
|
||||||
|
.n => 0x0E,
|
||||||
|
.o => 0x0F,
|
||||||
|
.p => 0x10,
|
||||||
|
.q => 0x11,
|
||||||
|
.r => 0x12,
|
||||||
|
.s => 0x13,
|
||||||
|
.t => 0x14,
|
||||||
|
.u => 0x15,
|
||||||
|
.v => 0x16,
|
||||||
|
.w => 0x17,
|
||||||
|
.x => 0x18,
|
||||||
|
.y => 0x19,
|
||||||
|
.z => 0x1A,
|
||||||
|
|
||||||
|
// These are purposely NOT handled here because of the fixterms
|
||||||
|
// specification: https://www.leonerd.org.uk/hacks/fixterms/
|
||||||
|
// These are processed as CSI u.
|
||||||
|
// .i => 0x09,
|
||||||
|
// .m => 0x0D,
|
||||||
|
// .left_bracket => 0x1B,
|
||||||
|
|
||||||
|
else => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This is the bitmask for fixterm CSI u modifiers.
|
||||||
|
const CsiUMods = packed struct(u3) {
|
||||||
|
shift: bool = false,
|
||||||
|
alt: bool = false,
|
||||||
|
ctrl: bool = false,
|
||||||
|
|
||||||
|
/// Convert an input mods value into the CSI u mods value.
|
||||||
|
pub fn fromInput(mods: key.Mods) CsiUMods {
|
||||||
|
return .{
|
||||||
|
.shift = mods.shift,
|
||||||
|
.alt = mods.alt,
|
||||||
|
.ctrl = mods.ctrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the raw int value of this packed struct.
|
||||||
|
pub fn int(self: CsiUMods) u3 {
|
||||||
|
return @bitCast(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the integer value sent as part of the CSI u sequence.
|
||||||
|
/// This adds 1 to the bitmask value as described in the spec.
|
||||||
|
pub fn seqInt(self: CsiUMods) u4 {
|
||||||
|
const raw: u4 = @intCast(self.int());
|
||||||
|
return raw + 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
test "modifer sequence values" {
|
||||||
|
// This is all sort of trivially seen by looking at the code but
|
||||||
|
// we want to make sure we never regress this.
|
||||||
|
var mods: CsiUMods = .{};
|
||||||
|
try testing.expectEqual(@as(u4, 1), mods.seqInt());
|
||||||
|
|
||||||
|
mods = .{ .shift = true };
|
||||||
|
try testing.expectEqual(@as(u4, 2), mods.seqInt());
|
||||||
|
|
||||||
|
mods = .{ .alt = true };
|
||||||
|
try testing.expectEqual(@as(u4, 3), mods.seqInt());
|
||||||
|
|
||||||
|
mods = .{ .ctrl = true };
|
||||||
|
try testing.expectEqual(@as(u4, 5), mods.seqInt());
|
||||||
|
|
||||||
|
mods = .{ .alt = true, .shift = true };
|
||||||
|
try testing.expectEqual(@as(u4, 4), mods.seqInt());
|
||||||
|
|
||||||
|
mods = .{ .ctrl = true, .shift = true };
|
||||||
|
try testing.expectEqual(@as(u4, 6), mods.seqInt());
|
||||||
|
|
||||||
|
mods = .{ .alt = true, .ctrl = true };
|
||||||
|
try testing.expectEqual(@as(u4, 7), mods.seqInt());
|
||||||
|
|
||||||
|
mods = .{ .alt = true, .ctrl = true, .shift = true };
|
||||||
|
try testing.expectEqual(@as(u4, 8), mods.seqInt());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "legacy: ctrl+alt+c" {
|
||||||
|
var buf: [128]u8 = undefined;
|
||||||
|
var enc: KeyEncoder = .{
|
||||||
|
.event = .{
|
||||||
|
.key = .c,
|
||||||
|
.mods = .{ .ctrl = true, .alt = true },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const actual = try enc.legacy(&buf);
|
||||||
|
try testing.expectEqualStrings("\x1b\x03", actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "legacy: ctrl+c" {
|
||||||
|
var buf: [128]u8 = undefined;
|
||||||
|
var enc: KeyEncoder = .{
|
||||||
|
.event = .{
|
||||||
|
.key = .c,
|
||||||
|
.mods = .{ .ctrl = true },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const actual = try enc.legacy(&buf);
|
||||||
|
try testing.expectEqualStrings("\x03", actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "legacy: ctrl+space" {
|
||||||
|
var buf: [128]u8 = undefined;
|
||||||
|
var enc: KeyEncoder = .{
|
||||||
|
.event = .{
|
||||||
|
.key = .space,
|
||||||
|
.mods = .{ .ctrl = true },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const actual = try enc.legacy(&buf);
|
||||||
|
try testing.expectEqualStrings("\x00", actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "legacy: ctrl+shift+backspace" {
|
||||||
|
var buf: [128]u8 = undefined;
|
||||||
|
var enc: KeyEncoder = .{
|
||||||
|
.event = .{
|
||||||
|
.key = .backspace,
|
||||||
|
.mods = .{ .ctrl = true, .shift = true },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const actual = try enc.legacy(&buf);
|
||||||
|
try testing.expectEqualStrings("\x08", actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "legacy: ctrl+shift+char with modify other state 2" {
|
||||||
|
var buf: [128]u8 = undefined;
|
||||||
|
var enc: KeyEncoder = .{
|
||||||
|
.event = .{
|
||||||
|
.key = .h,
|
||||||
|
.mods = .{ .ctrl = true, .shift = true },
|
||||||
|
.utf8 = "H",
|
||||||
|
},
|
||||||
|
.modify_other_keys_state_2 = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const actual = try enc.legacy(&buf);
|
||||||
|
try testing.expectEqualStrings("\x1b[27;6;72~", actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "legacy: fixterm awkward letters" {
|
||||||
|
var buf: [128]u8 = undefined;
|
||||||
|
{
|
||||||
|
var enc: KeyEncoder = .{ .event = .{
|
||||||
|
.key = .i,
|
||||||
|
.mods = .{ .ctrl = true },
|
||||||
|
.utf8 = "i",
|
||||||
|
} };
|
||||||
|
const actual = try enc.legacy(&buf);
|
||||||
|
try testing.expectEqualStrings("\x1b[105;5u", actual);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
var enc: KeyEncoder = .{ .event = .{
|
||||||
|
.key = .m,
|
||||||
|
.mods = .{ .ctrl = true },
|
||||||
|
.utf8 = "m",
|
||||||
|
} };
|
||||||
|
const actual = try enc.legacy(&buf);
|
||||||
|
try testing.expectEqualStrings("\x1b[109;5u", actual);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
var enc: KeyEncoder = .{ .event = .{
|
||||||
|
.key = .left_bracket,
|
||||||
|
.mods = .{ .ctrl = true },
|
||||||
|
.utf8 = "[",
|
||||||
|
} };
|
||||||
|
const actual = try enc.legacy(&buf);
|
||||||
|
try testing.expectEqualStrings("\x1b[91;5u", actual);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// This doesn't exactly match the fixterm spec but matches the
|
||||||
|
// behavior of Kitty.
|
||||||
|
var enc: KeyEncoder = .{ .event = .{
|
||||||
|
.key = .two,
|
||||||
|
.mods = .{ .ctrl = true, .shift = true },
|
||||||
|
.utf8 = "@",
|
||||||
|
} };
|
||||||
|
const actual = try enc.legacy(&buf);
|
||||||
|
try testing.expectEqualStrings("\x1b[64;6u", actual);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "ctrlseq: normal ctrl c" {
|
||||||
|
const seq = ctrlSeq(.c, .{ .ctrl = true });
|
||||||
|
try testing.expectEqual(@as(u8, 0x03), seq.?);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "ctrlseq: alt should be allowed" {
|
||||||
|
const seq = ctrlSeq(.c, .{ .alt = true, .ctrl = true });
|
||||||
|
try testing.expectEqual(@as(u8, 0x03), seq.?);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "ctrlseq: no ctrl does nothing" {
|
||||||
|
try testing.expect(ctrlSeq(.c, .{}) == null);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "ctrlseq: shift does not generate ctrl seq" {
|
||||||
|
try testing.expect(ctrlSeq(.c, .{ .shift = true }) == null);
|
||||||
|
try testing.expect(ctrlSeq(.c, .{ .shift = true, .ctrl = true }) == null);
|
||||||
|
}
|
@ -1,8 +1,55 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
/// A bitmask for all key modifiers. This is taken directly from the
|
/// A generic key input event. This is the information that is necessary
|
||||||
/// GLFW representation, but we use this generically.
|
/// regardless of apprt in order to generate the proper terminal
|
||||||
|
/// control sequences for a given key press.
|
||||||
|
///
|
||||||
|
/// Some apprts may not be able to provide all of this information, such
|
||||||
|
/// as GLFW. In this case, the apprt should provide as much information
|
||||||
|
/// as it can and it should be expected that the terminal behavior
|
||||||
|
/// will not be totally correct.
|
||||||
|
pub const KeyEvent = struct {
|
||||||
|
/// The action: press, release, etc.
|
||||||
|
action: Action = .press,
|
||||||
|
|
||||||
|
/// "key" is the logical key that was pressed. For example, if
|
||||||
|
/// a Dvorak keyboard layout is being used on a US keyboard,
|
||||||
|
/// the "i" physical key will be reported as "c". The physical
|
||||||
|
/// key is the key that was physically pressed on the keyboard.
|
||||||
|
key: Key,
|
||||||
|
physical_key: Key = .invalid,
|
||||||
|
|
||||||
|
/// Mods are the modifiers that are pressed.
|
||||||
|
mods: Mods = .{},
|
||||||
|
|
||||||
|
/// The mods that were consumed in order to generate the text
|
||||||
|
/// in utf8. This has the mods set that were consumed, so to
|
||||||
|
/// get the set of mods that are effective you must negate
|
||||||
|
/// mods with this.
|
||||||
|
///
|
||||||
|
/// This field is meaningless if utf8 is empty.
|
||||||
|
consumed_mods: Mods = .{},
|
||||||
|
|
||||||
|
/// Composing is true when this key event is part of a dead key
|
||||||
|
/// composition sequence and we're in the middle of it.
|
||||||
|
composing: bool = false,
|
||||||
|
|
||||||
|
/// The utf8 sequence that was generated by this key event.
|
||||||
|
/// This will be an empty string if there is no text generated.
|
||||||
|
/// If composing is true and this is non-empty, this is preedit
|
||||||
|
/// text.
|
||||||
|
utf8: []const u8 = "",
|
||||||
|
|
||||||
|
/// Returns the effective modifiers for this event. The effective
|
||||||
|
/// modifiers are the mods that should be considered for keybindings.
|
||||||
|
pub fn effectiveMods(self: KeyEvent) Mods {
|
||||||
|
if (self.utf8.len == 0) return self.mods;
|
||||||
|
return self.mods.unset(self.consumed_mods);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A bitmask for all key modifiers.
|
||||||
///
|
///
|
||||||
/// IMPORTANT: Any changes here update include/ghostty.h
|
/// IMPORTANT: Any changes here update include/ghostty.h
|
||||||
pub const Mods = packed struct(Mods.Backing) {
|
pub const Mods = packed struct(Mods.Backing) {
|
||||||
@ -56,6 +103,11 @@ pub const Mods = packed struct(Mods.Backing) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Perform `self &~ other` to remove the other mods from self.
|
||||||
|
pub fn unset(self: Mods, other: Mods) Mods {
|
||||||
|
return @bitCast(self.int() & ~other.int());
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the mods without locks set.
|
/// Returns the mods without locks set.
|
||||||
pub fn withoutLocks(self: Mods) Mods {
|
pub fn withoutLocks(self: Mods) Mods {
|
||||||
var copy = self;
|
var copy = self;
|
||||||
|
Reference in New Issue
Block a user