mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
366
src/Surface.zig
366
src/Surface.zig
@ -72,6 +72,9 @@ renderer_thr: std.Thread,
|
|||||||
/// Mouse state.
|
/// Mouse state.
|
||||||
mouse: Mouse,
|
mouse: Mouse,
|
||||||
|
|
||||||
|
/// Keyboard input state.
|
||||||
|
keyboard: Keyboard,
|
||||||
|
|
||||||
/// A currently pressed key. This is used so that we can send a keyboard
|
/// A currently pressed key. This is used so that we can send a keyboard
|
||||||
/// release event when the surface is unfocused. Note that when the surface
|
/// release event when the surface is unfocused. Note that when the surface
|
||||||
/// is refocused, a key press event may not be sent again -- this depends
|
/// is refocused, a key press event may not be sent again -- this depends
|
||||||
@ -192,6 +195,30 @@ const Mouse = struct {
|
|||||||
link_point: ?terminal.point.Coordinate = null,
|
link_point: ?terminal.point.Coordinate = null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Keyboard state for the surface.
|
||||||
|
pub const Keyboard = struct {
|
||||||
|
/// The currently active keybindings for the surface. This is used to
|
||||||
|
/// implement sequences: as leader keys are pressed, the active bindings
|
||||||
|
/// set is updated to reflect the current leader key sequence. If this is
|
||||||
|
/// null then the root bindings are used.
|
||||||
|
bindings: ?*const input.Binding.Set = null,
|
||||||
|
|
||||||
|
/// The last handled binding. This is used to prevent encoding release
|
||||||
|
/// events for handled bindings. We only need to keep track of one because
|
||||||
|
/// at least at the time of writing this, its impossible for two keys of
|
||||||
|
/// a combination to be handled by different bindings before the release
|
||||||
|
/// of the prior (namely since you can't bind modifier-only).
|
||||||
|
last_trigger: ?u64 = null,
|
||||||
|
|
||||||
|
/// The queued keys when we're in the middle of a sequenced binding.
|
||||||
|
/// These are flushed when the sequence is completed and unconsumed or
|
||||||
|
/// invalid.
|
||||||
|
///
|
||||||
|
/// This is naturally bounded due to the configuration maximum
|
||||||
|
/// length of a sequence.
|
||||||
|
queued: std.ArrayListUnmanaged(termio.Message.WriteReq) = .{},
|
||||||
|
};
|
||||||
|
|
||||||
/// The configuration that a surface has, this is copied from the main
|
/// The configuration that a surface has, this is copied from the main
|
||||||
/// Config struct usually to prevent sharing a single value.
|
/// Config struct usually to prevent sharing a single value.
|
||||||
const DerivedConfig = struct {
|
const DerivedConfig = struct {
|
||||||
@ -428,6 +455,7 @@ pub fn init(
|
|||||||
},
|
},
|
||||||
.renderer_thr = undefined,
|
.renderer_thr = undefined,
|
||||||
.mouse = .{},
|
.mouse = .{},
|
||||||
|
.keyboard = .{},
|
||||||
.io = undefined,
|
.io = undefined,
|
||||||
.io_thread = io_thread,
|
.io_thread = io_thread,
|
||||||
.io_thr = undefined,
|
.io_thr = undefined,
|
||||||
@ -605,6 +633,10 @@ pub fn deinit(self: *Surface) void {
|
|||||||
self.alloc.destroy(v);
|
self.alloc.destroy(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean up our keyboard state
|
||||||
|
for (self.keyboard.queued.items) |req| req.deinit();
|
||||||
|
self.keyboard.queued.deinit(self.alloc);
|
||||||
|
|
||||||
// Clean up our font grid
|
// Clean up our font grid
|
||||||
self.app.font_grid_set.deref(self.font_grid_key);
|
self.app.font_grid_set.deref(self.font_grid_key);
|
||||||
|
|
||||||
@ -857,6 +889,10 @@ fn changeConfig(self: *Surface, config: *const configpkg.Config) !void {
|
|||||||
self.showMouse();
|
self.showMouse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we are in the middle of a key sequence, clear it.
|
||||||
|
self.keyboard.bindings = null;
|
||||||
|
self.endKeySequence(.drop, .free);
|
||||||
|
|
||||||
// Before sending any other config changes, we give the renderer a new font
|
// Before sending any other config changes, we give the renderer a new font
|
||||||
// grid. We could check to see if there was an actual change to the font,
|
// grid. We could check to see if there was an actual change to the font,
|
||||||
// but this is easier and pretty rare so it's not a performance concern.
|
// but this is easier and pretty rare so it's not a performance concern.
|
||||||
@ -1322,76 +1358,13 @@ pub fn keyCallback(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Before encoding, we see if we have any keybindings for this
|
// Handle keybindings first. We need to handle this on all events
|
||||||
// key. Those always intercept before any encoding tasks.
|
// (press, repeat, release) because a press may perform a binding but
|
||||||
binding: {
|
// a release should not encode if we consumed the press.
|
||||||
const binding_action: input.Binding.Action, const binding_trigger: input.Binding.Trigger, const consumed = action: {
|
if (try self.maybeHandleBinding(
|
||||||
const binding_mods = event.mods.binding();
|
event,
|
||||||
var trigger: input.Binding.Trigger = .{
|
if (insp_ev) |*ev| ev else null,
|
||||||
.mods = binding_mods,
|
)) |v| return v;
|
||||||
.key = .{ .translated = event.key },
|
|
||||||
};
|
|
||||||
|
|
||||||
const set = self.config.keybind.set;
|
|
||||||
if (set.get(trigger)) |v| break :action .{
|
|
||||||
v,
|
|
||||||
trigger,
|
|
||||||
set.getConsumed(trigger),
|
|
||||||
};
|
|
||||||
|
|
||||||
trigger.key = .{ .physical = event.physical_key };
|
|
||||||
if (set.get(trigger)) |v| break :action .{
|
|
||||||
v,
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
// We only execute the binding on press/repeat but we still consume
|
|
||||||
// the key on release so that we don't send any release events.
|
|
||||||
log.debug("key event binding consumed={} action={}", .{ consumed, binding_action });
|
|
||||||
const performed = if (event.action == .press or event.action == .repeat) press: {
|
|
||||||
self.last_binding_trigger = 0;
|
|
||||||
break :press try self.performBindingAction(binding_action);
|
|
||||||
} else false;
|
|
||||||
|
|
||||||
// If we performed an action and it was a closing action,
|
|
||||||
// our "self" pointer is not safe to use anymore so we need to
|
|
||||||
// just exit immediately.
|
|
||||||
if (performed and closingAction(binding_action)) {
|
|
||||||
log.debug("key binding is a closing binding, halting key event processing", .{});
|
|
||||||
return .closed;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we consume this event, then we are done. If we don't consume
|
|
||||||
// it, we processed the action but we still want to process our
|
|
||||||
// encodings, too.
|
|
||||||
if (consumed and performed) {
|
|
||||||
self.last_binding_trigger = binding_trigger.hash();
|
|
||||||
if (insp_ev) |*ev| ev.binding = binding_action;
|
|
||||||
return .consumed;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have a previous binding trigger and it matches this one,
|
|
||||||
// then we handled the down event so we don't want to send any further
|
|
||||||
// events.
|
|
||||||
if (self.last_binding_trigger > 0 and
|
|
||||||
self.last_binding_trigger == binding_trigger.hash())
|
|
||||||
{
|
|
||||||
return .consumed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we allow KAM and KAM is enabled then we do nothing.
|
// If we allow KAM and KAM is enabled then we do nothing.
|
||||||
if (self.config.vt_kam_allowed) {
|
if (self.config.vt_kam_allowed) {
|
||||||
@ -1461,26 +1434,6 @@ pub fn keyCallback(
|
|||||||
}).keyToMouseShape()) |shape|
|
}).keyToMouseShape()) |shape|
|
||||||
try self.rt_surface.setMouseShape(shape);
|
try self.rt_surface.setMouseShape(shape);
|
||||||
|
|
||||||
// No binding, so we have to perform an encoding task. This
|
|
||||||
// may still result in no encoding. Under different modes and
|
|
||||||
// inputs there are many keybindings that result in no encoding
|
|
||||||
// whatsoever.
|
|
||||||
const enc: input.KeyEncoder = enc: {
|
|
||||||
self.renderer_state.mutex.lock();
|
|
||||||
defer self.renderer_state.mutex.unlock();
|
|
||||||
const t = &self.io.terminal;
|
|
||||||
break :enc .{
|
|
||||||
.event = event,
|
|
||||||
.macos_option_as_alt = self.config.macos_option_as_alt,
|
|
||||||
.alt_esc_prefix = t.modes.get(.alt_esc_prefix),
|
|
||||||
.cursor_key_application = t.modes.get(.cursor_keys),
|
|
||||||
.keypad_key_application = t.modes.get(.keypad_keys),
|
|
||||||
.ignore_keypad_with_numlock = t.modes.get(.ignore_keypad_with_numlock),
|
|
||||||
.modify_other_keys_state_2 = t.flags.modify_other_keys_2,
|
|
||||||
.kitty_flags = t.screen.kitty_keyboard.current(),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// We've processed a key event that produced some data so we want to
|
// We've processed a key event that produced some data so we want to
|
||||||
// track the last pressed key.
|
// track the last pressed key.
|
||||||
self.pressed_key = event: {
|
self.pressed_key = event: {
|
||||||
@ -1504,6 +1457,216 @@ pub fn keyCallback(
|
|||||||
break :event copy;
|
break :event copy;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Encode and send our key. If we didn't encode anything, then we
|
||||||
|
// return the effect as ignored.
|
||||||
|
if (try self.encodeKey(
|
||||||
|
event,
|
||||||
|
if (insp_ev) |*ev| ev else null,
|
||||||
|
)) |write_req| {
|
||||||
|
errdefer write_req.deinit();
|
||||||
|
self.io.queueMessage(switch (write_req) {
|
||||||
|
.small => |v| .{ .write_small = v },
|
||||||
|
.stable => |v| .{ .write_stable = v },
|
||||||
|
.alloc => |v| .{ .write_alloc = v },
|
||||||
|
}, .unlocked);
|
||||||
|
} else {
|
||||||
|
// No valid request means that we didn't encode anything.
|
||||||
|
return .ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If our event is any keypress that isn't a modifier and we generated
|
||||||
|
// some data to send to the pty, then we move the viewport down to the
|
||||||
|
// bottom. We also clear the selection for any key other then modifiers.
|
||||||
|
if (!event.key.modifier()) {
|
||||||
|
self.renderer_state.mutex.lock();
|
||||||
|
defer self.renderer_state.mutex.unlock();
|
||||||
|
try self.setSelection(null);
|
||||||
|
try self.io.terminal.scrollViewport(.{ .bottom = {} });
|
||||||
|
try self.queueRender();
|
||||||
|
}
|
||||||
|
|
||||||
|
return .consumed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Maybe handles a binding for a given event and if so returns the effect.
|
||||||
|
/// Returns null if the event is not handled in any way and processing should
|
||||||
|
/// continue.
|
||||||
|
fn maybeHandleBinding(
|
||||||
|
self: *Surface,
|
||||||
|
event: input.KeyEvent,
|
||||||
|
insp_ev: ?*inspector.key.Event,
|
||||||
|
) !?InputEffect {
|
||||||
|
switch (event.action) {
|
||||||
|
// Release events never trigger a binding but we need to check if
|
||||||
|
// we consumed the press event so we don't encode the release.
|
||||||
|
.release => {
|
||||||
|
if (self.keyboard.last_trigger) |last| {
|
||||||
|
if (last == event.bindingHash()) {
|
||||||
|
// We don't reset the last trigger on release because
|
||||||
|
// an apprt may send multiple release events for a single
|
||||||
|
// press event.
|
||||||
|
return .consumed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Carry on processing.
|
||||||
|
.press, .repeat => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find an entry in the keybind set that matches our event.
|
||||||
|
const entry: input.Binding.Set.Entry = entry: {
|
||||||
|
const set = self.keyboard.bindings orelse &self.config.keybind.set;
|
||||||
|
|
||||||
|
var trigger: input.Binding.Trigger = .{
|
||||||
|
.mods = event.mods.binding(),
|
||||||
|
.key = .{ .translated = event.key },
|
||||||
|
};
|
||||||
|
if (set.get(trigger)) |v| break :entry v;
|
||||||
|
|
||||||
|
trigger.key = .{ .physical = event.physical_key };
|
||||||
|
if (set.get(trigger)) |v| break :entry v;
|
||||||
|
|
||||||
|
if (event.unshifted_codepoint > 0) {
|
||||||
|
trigger.key = .{ .unicode = event.unshifted_codepoint };
|
||||||
|
if (set.get(trigger)) |v| break :entry v;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No entry found. If we're not looking at the root set of the
|
||||||
|
// bindings we need to encode everything up to this point and
|
||||||
|
// send to the pty.
|
||||||
|
if (self.keyboard.bindings != null) {
|
||||||
|
// Reset to the root set
|
||||||
|
self.keyboard.bindings = null;
|
||||||
|
|
||||||
|
// Encode everything up to this point
|
||||||
|
self.endKeySequence(.flush, .retain);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Determine if this entry has an action or if its a leader key.
|
||||||
|
const action: input.Binding.Action, const consumed: bool = switch (entry) {
|
||||||
|
.leader => |set| {
|
||||||
|
// Setup the next set we'll look at.
|
||||||
|
self.keyboard.bindings = set;
|
||||||
|
|
||||||
|
// Store this event so that we can drain and encode on invalid.
|
||||||
|
// We don't need to cap this because it is naturally capped by
|
||||||
|
// the config validation.
|
||||||
|
if (try self.encodeKey(event, insp_ev)) |req| {
|
||||||
|
try self.keyboard.queued.append(self.alloc, req);
|
||||||
|
}
|
||||||
|
|
||||||
|
return .consumed;
|
||||||
|
},
|
||||||
|
|
||||||
|
.action => |v| .{ v, true },
|
||||||
|
.action_unconsumed => |v| .{ v, false },
|
||||||
|
};
|
||||||
|
|
||||||
|
// We have an action, so at this point we're handling SOMETHING so
|
||||||
|
// we reset the last trigger to null. We only set this if we actually
|
||||||
|
// perform an action (below)
|
||||||
|
self.keyboard.last_trigger = null;
|
||||||
|
|
||||||
|
// An action also always resets the binding set.
|
||||||
|
self.keyboard.bindings = null;
|
||||||
|
|
||||||
|
// Attempt to perform the action
|
||||||
|
log.debug("key event binding consumed={} action={}", .{ consumed, action });
|
||||||
|
const performed = try self.performBindingAction(action);
|
||||||
|
|
||||||
|
// If we performed an action and it was a closing action,
|
||||||
|
// our "self" pointer is not safe to use anymore so we need to
|
||||||
|
// just exit immediately.
|
||||||
|
if (performed and closingAction(action)) {
|
||||||
|
log.debug("key binding is a closing binding, halting key event processing", .{});
|
||||||
|
return .closed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we consume this event, then we are done. If we don't consume
|
||||||
|
// it, we processed the action but we still want to process our
|
||||||
|
// encodings, too.
|
||||||
|
if (performed and consumed) {
|
||||||
|
// If we had queued events, we deinit them since we consumed
|
||||||
|
self.endKeySequence(.drop, .retain);
|
||||||
|
|
||||||
|
// Store our last trigger so we don't encode the release event
|
||||||
|
self.keyboard.last_trigger = event.bindingHash();
|
||||||
|
|
||||||
|
if (insp_ev) |ev| ev.binding = action;
|
||||||
|
return .consumed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we didn't perform OR we didn't consume, then we want to
|
||||||
|
// encode any queued events for a sequence.
|
||||||
|
self.endKeySequence(.flush, .retain);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const KeySequenceQueued = enum { flush, drop };
|
||||||
|
const KeySequenceMemory = enum { retain, free };
|
||||||
|
|
||||||
|
/// End a key sequence. Safe to call if no key sequence is active.
|
||||||
|
///
|
||||||
|
/// Action and mem determine the behavior of the queued inputs up to this
|
||||||
|
/// point.
|
||||||
|
fn endKeySequence(
|
||||||
|
self: *Surface,
|
||||||
|
action: KeySequenceQueued,
|
||||||
|
mem: KeySequenceMemory,
|
||||||
|
) void {
|
||||||
|
if (self.keyboard.queued.items.len > 0) {
|
||||||
|
switch (action) {
|
||||||
|
.flush => for (self.keyboard.queued.items) |write_req| {
|
||||||
|
self.io.queueMessage(switch (write_req) {
|
||||||
|
.small => |v| .{ .write_small = v },
|
||||||
|
.stable => |v| .{ .write_stable = v },
|
||||||
|
.alloc => |v| .{ .write_alloc = v },
|
||||||
|
}, .unlocked);
|
||||||
|
},
|
||||||
|
|
||||||
|
.drop => for (self.keyboard.queued.items) |req| req.deinit(),
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (mem) {
|
||||||
|
.free => self.keyboard.queued.clearAndFree(self.alloc),
|
||||||
|
.retain => self.keyboard.queued.clearRetainingCapacity(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encodes the key event into a write request. The write request will
|
||||||
|
/// always copy or allocate so the caller can safely free the event.
|
||||||
|
fn encodeKey(
|
||||||
|
self: *Surface,
|
||||||
|
event: input.KeyEvent,
|
||||||
|
insp_ev: ?*inspector.key.Event,
|
||||||
|
) !?termio.Message.WriteReq {
|
||||||
|
// Build up our encoder. Under different modes and
|
||||||
|
// inputs there are many keybindings that result in no encoding
|
||||||
|
// whatsoever.
|
||||||
|
const enc: input.KeyEncoder = enc: {
|
||||||
|
self.renderer_state.mutex.lock();
|
||||||
|
defer self.renderer_state.mutex.unlock();
|
||||||
|
const t = &self.io.terminal;
|
||||||
|
break :enc .{
|
||||||
|
.event = event,
|
||||||
|
.macos_option_as_alt = self.config.macos_option_as_alt,
|
||||||
|
.alt_esc_prefix = t.modes.get(.alt_esc_prefix),
|
||||||
|
.cursor_key_application = t.modes.get(.cursor_keys),
|
||||||
|
.keypad_key_application = t.modes.get(.keypad_keys),
|
||||||
|
.ignore_keypad_with_numlock = t.modes.get(.ignore_keypad_with_numlock),
|
||||||
|
.modify_other_keys_state_2 = t.flags.modify_other_keys_2,
|
||||||
|
.kitty_flags = t.screen.kitty_keyboard.current(),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const write_req: termio.Message.WriteReq = req: {
|
const write_req: termio.Message.WriteReq = req: {
|
||||||
// Try to write the input into a small array. This fits almost
|
// Try to write the input into a small array. This fits almost
|
||||||
// every scenario. Larger situations can happen due to long
|
// every scenario. Larger situations can happen due to long
|
||||||
@ -1511,7 +1674,7 @@ pub fn keyCallback(
|
|||||||
var data: termio.Message.WriteReq.Small.Array = undefined;
|
var data: termio.Message.WriteReq.Small.Array = undefined;
|
||||||
if (enc.encode(&data)) |seq| {
|
if (enc.encode(&data)) |seq| {
|
||||||
// Special-case: we did nothing.
|
// Special-case: we did nothing.
|
||||||
if (seq.len == 0) return .ignored;
|
if (seq.len == 0) return null;
|
||||||
|
|
||||||
break :req .{ .small = .{
|
break :req .{ .small = .{
|
||||||
.data = data,
|
.data = data,
|
||||||
@ -1544,7 +1707,7 @@ pub fn keyCallback(
|
|||||||
// Copy the encoded data into the inspector event if we have one.
|
// Copy the encoded data into the inspector event if we have one.
|
||||||
// We do this before the mailbox because the IO thread could
|
// We do this before the mailbox because the IO thread could
|
||||||
// release the memory before we get a chance to copy it.
|
// release the memory before we get a chance to copy it.
|
||||||
if (insp_ev) |*ev| pty: {
|
if (insp_ev) |ev| pty: {
|
||||||
const slice = write_req.slice();
|
const slice = write_req.slice();
|
||||||
const copy = self.alloc.alloc(u8, slice.len) catch |err| {
|
const copy = self.alloc.alloc(u8, slice.len) catch |err| {
|
||||||
log.warn("error allocating pty data for inspector err={}", .{err});
|
log.warn("error allocating pty data for inspector err={}", .{err});
|
||||||
@ -1555,24 +1718,7 @@ pub fn keyCallback(
|
|||||||
ev.pty = copy;
|
ev.pty = copy;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.io.queueMessage(switch (write_req) {
|
return write_req;
|
||||||
.small => |v| .{ .write_small = v },
|
|
||||||
.stable => |v| .{ .write_stable = v },
|
|
||||||
.alloc => |v| .{ .write_alloc = v },
|
|
||||||
}, .unlocked);
|
|
||||||
|
|
||||||
// If our event is any keypress that isn't a modifier and we generated
|
|
||||||
// some data to send to the pty, then we move the viewport down to the
|
|
||||||
// bottom. We also clear the selection for any key other then modifiers.
|
|
||||||
if (!event.key.modifier()) {
|
|
||||||
self.renderer_state.mutex.lock();
|
|
||||||
defer self.renderer_state.mutex.unlock();
|
|
||||||
try self.setSelection(null);
|
|
||||||
try self.io.terminal.scrollViewport(.{ .bottom = {} });
|
|
||||||
try self.queueRender();
|
|
||||||
}
|
|
||||||
|
|
||||||
return .consumed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sends text as-is to the terminal without triggering any keyboard
|
/// Sends text as-is to the terminal without triggering any keyboard
|
||||||
|
@ -117,13 +117,17 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 {
|
|||||||
var widest_key: usize = 0;
|
var widest_key: usize = 0;
|
||||||
var buf: [64]u8 = undefined;
|
var buf: [64]u8 = undefined;
|
||||||
while (iter.next()) |bind| {
|
while (iter.next()) |bind| {
|
||||||
|
const action = switch (bind.value_ptr.*) {
|
||||||
|
.leader => continue, // TODO: support this
|
||||||
|
.action, .action_unconsumed => |action| action,
|
||||||
|
};
|
||||||
const key = switch (bind.key_ptr.key) {
|
const key = switch (bind.key_ptr.key) {
|
||||||
.translated => |k| try std.fmt.bufPrint(&buf, "{s}", .{@tagName(k)}),
|
.translated => |k| try std.fmt.bufPrint(&buf, "{s}", .{@tagName(k)}),
|
||||||
.physical => |k| try std.fmt.bufPrint(&buf, "physical:{s}", .{@tagName(k)}),
|
.physical => |k| try std.fmt.bufPrint(&buf, "physical:{s}", .{@tagName(k)}),
|
||||||
.unicode => |c| try std.fmt.bufPrint(&buf, "{u}", .{c}),
|
.unicode => |c| try std.fmt.bufPrint(&buf, "{u}", .{c}),
|
||||||
};
|
};
|
||||||
widest_key = @max(widest_key, win.gwidth(key));
|
widest_key = @max(widest_key, win.gwidth(key));
|
||||||
try bindings.append(.{ .trigger = bind.key_ptr.*, .action = bind.value_ptr.* });
|
try bindings.append(.{ .trigger = bind.key_ptr.*, .action = action });
|
||||||
}
|
}
|
||||||
std.mem.sort(Binding, bindings.items, {}, Binding.lessThan);
|
std.mem.sort(Binding, bindings.items, {}, Binding.lessThan);
|
||||||
|
|
||||||
|
@ -618,6 +618,36 @@ class: ?[:0]const u8 = null,
|
|||||||
/// or the alias. When debugging keybinds, the non-aliased modifier will always
|
/// or the alias. When debugging keybinds, the non-aliased modifier will always
|
||||||
/// be used in output.
|
/// be used in output.
|
||||||
///
|
///
|
||||||
|
/// You may also specify multiple triggers separated by `>` to require a
|
||||||
|
/// sequence of triggers to activate the action. For example,
|
||||||
|
/// `ctrl+a>n=new_window` will only trigger the `new_window` action if the
|
||||||
|
/// user presses `ctrl+a` followed separately by `n`. In other software, this
|
||||||
|
/// is sometimes called a leader key, a key chord, a key table, etc. There
|
||||||
|
/// is no hardcoded limit on the number of parts in a sequence.
|
||||||
|
///
|
||||||
|
/// Warning: if you define a sequence as a CLI argument to `ghostty`,
|
||||||
|
/// you probably have to quote the keybind since `>` is a special character
|
||||||
|
/// in most shells. Example: ghostty --keybind='ctrl+a>n=new_window'
|
||||||
|
///
|
||||||
|
/// A trigger sequence has some special handling:
|
||||||
|
///
|
||||||
|
/// * Ghostty will wait an indefinite amount of time for the next key in
|
||||||
|
/// the sequence. There is no way to specify a timeout. The only way to
|
||||||
|
/// force the output of a prefix key is to assign another keybind to
|
||||||
|
/// specifically output that key (i.e. `ctrl+a>ctrl+a=text:foo`) or
|
||||||
|
/// press an unbound key which will send both keys to the program.
|
||||||
|
///
|
||||||
|
/// * If a prefix in a sequence is previously bound, the sequence will
|
||||||
|
/// override the previous binding. For example, if `ctrl+a` is bound to
|
||||||
|
/// `new_window` and `ctrl+a>n` is bound to `new_tab`, pressing `ctrl+a`
|
||||||
|
/// will do nothing.
|
||||||
|
///
|
||||||
|
/// * Adding to the above, if a previously bound sequence prefix is
|
||||||
|
/// used in a new, non-sequence binding, the entire previously bound
|
||||||
|
/// sequence will be unbound. For example, if you bind `ctrl+a>n` and
|
||||||
|
/// `ctrl+a>t`, and then bind `ctrl+a` directly, both `ctrl+a>n` and
|
||||||
|
/// `ctrl+a>t` will become unbound.
|
||||||
|
///
|
||||||
/// Action is the action to take when the trigger is satisfied. It takes the
|
/// Action is the action to take when the trigger is satisfied. It takes the
|
||||||
/// format `action` or `action:param`. The latter form is only valid if the
|
/// format `action` or `action:param`. The latter form is only valid if the
|
||||||
/// action requires a parameter.
|
/// action requires a parameter.
|
||||||
@ -3311,43 +3341,56 @@ pub const Keybinds = struct {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const binding = try inputpkg.Binding.parse(value);
|
// Let our much better tested binding package handle parsing and storage.
|
||||||
switch (binding.action) {
|
try self.set.parseAndPut(alloc, value);
|
||||||
.unbind => self.set.remove(binding.trigger),
|
|
||||||
else => if (binding.consumed) {
|
|
||||||
try self.set.put(alloc, binding.trigger, binding.action);
|
|
||||||
} else {
|
|
||||||
try self.set.putUnconsumed(alloc, binding.trigger, binding.action);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deep copy of the struct. Required by Config.
|
/// Deep copy of the struct. Required by Config.
|
||||||
pub fn clone(self: *const Keybinds, alloc: Allocator) !Keybinds {
|
pub fn clone(self: *const Keybinds, alloc: Allocator) !Keybinds {
|
||||||
return .{
|
return .{ .set = try self.set.clone(alloc) };
|
||||||
.set = .{
|
|
||||||
.bindings = try self.set.bindings.clone(alloc),
|
|
||||||
.reverse = try self.set.reverse.clone(alloc),
|
|
||||||
.unconsumed = try self.set.unconsumed.clone(alloc),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compare if two of our value are requal. Required by Config.
|
/// Compare if two of our value are requal. Required by Config.
|
||||||
pub fn equal(self: Keybinds, other: Keybinds) bool {
|
pub fn equal(self: Keybinds, other: Keybinds) bool {
|
||||||
const self_map = self.set.bindings;
|
return equalSet(&self.set, &other.set);
|
||||||
const other_map = other.set.bindings;
|
}
|
||||||
|
|
||||||
|
fn equalSet(
|
||||||
|
self: *const inputpkg.Binding.Set,
|
||||||
|
other: *const inputpkg.Binding.Set,
|
||||||
|
) bool {
|
||||||
|
// Two keybinds are considered equal if their primary bindings
|
||||||
|
// are the same. We don't compare reverse mappings and such.
|
||||||
|
const self_map = &self.bindings;
|
||||||
|
const other_map = &other.bindings;
|
||||||
|
|
||||||
|
// If the count of mappings isn't identical they can't be equal
|
||||||
if (self_map.count() != other_map.count()) return false;
|
if (self_map.count() != other_map.count()) return false;
|
||||||
|
|
||||||
var it = self_map.iterator();
|
var it = self_map.iterator();
|
||||||
while (it.next()) |self_entry| {
|
while (it.next()) |self_entry| {
|
||||||
|
// If the trigger isn't in the other map, they can't be equal
|
||||||
const other_entry = other_map.getEntry(self_entry.key_ptr.*) orelse
|
const other_entry = other_map.getEntry(self_entry.key_ptr.*) orelse
|
||||||
return false;
|
return false;
|
||||||
if (!equalField(
|
|
||||||
inputpkg.Binding.Action,
|
// If the entry types are different, they can't be equal
|
||||||
self_entry.value_ptr.*,
|
if (std.meta.activeTag(self_entry.value_ptr.*) !=
|
||||||
other_entry.value_ptr.*,
|
std.meta.activeTag(other_entry.value_ptr.*)) return false;
|
||||||
)) return false;
|
|
||||||
|
switch (self_entry.value_ptr.*) {
|
||||||
|
// They're equal if both leader sets are equal.
|
||||||
|
.leader => if (!equalSet(
|
||||||
|
self_entry.value_ptr.*.leader,
|
||||||
|
other_entry.value_ptr.*.leader,
|
||||||
|
)) return false,
|
||||||
|
|
||||||
|
// Actions are compared by field directly
|
||||||
|
inline .action, .action_unconsumed => |_, tag| if (!equalField(
|
||||||
|
inputpkg.Binding.Action,
|
||||||
|
@field(self_entry.value_ptr.*, @tagName(tag)),
|
||||||
|
@field(other_entry.value_ptr.*, @tagName(tag)),
|
||||||
|
)) return false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -54,6 +54,29 @@ pub const KeyEvent = struct {
|
|||||||
if (self.utf8.len == 0) return self.mods;
|
if (self.utf8.len == 0) return self.mods;
|
||||||
return self.mods.unset(self.consumed_mods);
|
return self.mods.unset(self.consumed_mods);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a unique hash for this key event to be used for tracking
|
||||||
|
/// uniquess specifically with bindings. This omits fields that are
|
||||||
|
/// irrelevant for bindings.
|
||||||
|
pub fn bindingHash(self: KeyEvent) u64 {
|
||||||
|
var hasher = std.hash.Wyhash.init(0);
|
||||||
|
|
||||||
|
// These are all the fields that are explicitly part of Trigger.
|
||||||
|
std.hash.autoHash(&hasher, self.key);
|
||||||
|
std.hash.autoHash(&hasher, self.physical_key);
|
||||||
|
std.hash.autoHash(&hasher, self.unshifted_codepoint);
|
||||||
|
std.hash.autoHash(&hasher, self.mods.binding());
|
||||||
|
|
||||||
|
// Notes on unmapped things and why:
|
||||||
|
//
|
||||||
|
// - action: we don't have action-specific bindings right now
|
||||||
|
// AND we want to know if a key resulted in a binding regardless
|
||||||
|
// of action because a press should also ignore a release and so on.
|
||||||
|
//
|
||||||
|
// We can add to this if there is other confusion.
|
||||||
|
|
||||||
|
return hasher.final();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A bitmask for all key modifiers.
|
/// A bitmask for all key modifiers.
|
||||||
|
@ -179,7 +179,7 @@ pub fn MessageData(comptime Elem: type, comptime small_size: comptime_int) type
|
|||||||
pub fn deinit(self: Self) void {
|
pub fn deinit(self: Self) void {
|
||||||
switch (self) {
|
switch (self) {
|
||||||
.small, .stable => {},
|
.small, .stable => {},
|
||||||
.alloc => |v| v.alloc.free(v.alloc.data),
|
.alloc => |v| v.alloc.free(v.data),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user