mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-10-22 05:02:31 +00:00
Add remap_modifier to present one modifier key to kitty as another
remap_modifier <from> <to>
Repeatable and off by default. The declarations compose as a single
simultaneous permutation applied once per event, so
remap_modifier ctrl super
remap_modifier super ctrl
exchanges the two rather than collapsing to the identity. Only shift, alt,
ctrl, super, hyper and meta may be named; anything else is rejected with an
error. The last declaration for a given source wins.
The remap is applied to the modifier mask BEFORE the platform produces the
keysym and text - in glfw_xkb_update_modifiers() before xkb_state_update_mask()
on Linux, and in remap_cocoa_flags() before UCKeyTranslate() and
interpretKeyEvents: on macOS. Applying it to the finished event instead
desynchronises the modifiers from the text for any modifier that participates
in text production: shift on Linux, and Option on macOS, which produces text
natively unless macos_option_as_alt is set.
Because the GLFW modifiers are themselves derived from that state, this is
application wide: kitty's own shortcuts, kitty_mod and the bytes sent to the
child all see the remapped modifier, and every mapping in kitty.conf is written
in terms of the modifier a key becomes. X11 pointer events do not go through
xkb, so translateState() - which only ever sees button and DND events - gets
the same treatment; Wayland pointer events already carry xkb-derived modifiers.
A modifier key's own event has its identity remapped too, so a program using
the full keyboard protocol does not see ctrl reported together with LEFT_HYPER.
The press/release state fixup on Linux is given the remapped modifier rather
than the physical one, so press and release agree.
--debug-input reports each remapped event.
Notes: on Wayland kitty only detects hyper and meta when
KITTY_WAYLAND_DETECT_MODIFIERS is set, so remapping to or from them has nothing
to act on there. On macOS hyper and meta have no Cocoa modifier flag, so a
remap onto them is left unapplied rather than silently dropping the modifier,
and interpretKeyEvents: is only approximately corrected because the event's
characters strings were computed by the OS from the physical flags; the
UCKeyTranslate() path, normally the one taken, is exact.
Generated option files and the glfw wrapper come from their generators.
This commit is contained in:
parent
9f5cc8a484
commit
9d678fa9f2
17 changed files with 397 additions and 20 deletions
|
|
@ -516,6 +516,58 @@ translateFlags(NSUInteger flags)
|
|||
return mods;
|
||||
}
|
||||
|
||||
// Apply remap_modifier to Cocoa modifier flags. This has to happen BEFORE the
|
||||
// flags are used to produce text - UCKeyTranslate() below, and Cocoa's own
|
||||
// interpretKeyEvents: - for the same reason as on Linux: Option produces text
|
||||
// natively (see macos_option_as_alt), so remapping only the finished event's
|
||||
// modifier bitmask would leave the modifiers and the text contradicting each
|
||||
// other.
|
||||
//
|
||||
// Only the flags that GLFW models as remappable modifiers are touched; caps
|
||||
// lock, function, numeric pad and the device-dependent bits pass through.
|
||||
// GLFW_MOD_HYPER and GLFW_MOD_META have no Cocoa flag, so a remap onto them
|
||||
// cannot be represented here and is left alone rather than silently dropped.
|
||||
static NSUInteger
|
||||
remap_cocoa_flags(NSUInteger flags) {
|
||||
if (!_glfw.modifierRemapMask) return flags;
|
||||
int mods = 0;
|
||||
if (flags & NSEventModifierFlagShift) mods |= GLFW_MOD_SHIFT;
|
||||
if (flags & NSEventModifierFlagControl) mods |= GLFW_MOD_CONTROL;
|
||||
if (flags & NSEventModifierFlagOption) mods |= GLFW_MOD_ALT;
|
||||
if (flags & NSEventModifierFlagCommand) mods |= GLFW_MOD_SUPER;
|
||||
const int remapped = _glfwApplyModifierRemap(mods);
|
||||
if (remapped == mods) return flags;
|
||||
if (remapped & (GLFW_MOD_HYPER | GLFW_MOD_META)) return flags;
|
||||
NSUInteger ans = flags & ~(NSUInteger)(NSEventModifierFlagShift | NSEventModifierFlagControl |
|
||||
NSEventModifierFlagOption | NSEventModifierFlagCommand);
|
||||
if (remapped & GLFW_MOD_SHIFT) ans |= NSEventModifierFlagShift;
|
||||
if (remapped & GLFW_MOD_CONTROL) ans |= NSEventModifierFlagControl;
|
||||
if (remapped & GLFW_MOD_ALT) ans |= NSEventModifierFlagOption;
|
||||
if (remapped & GLFW_MOD_SUPER) ans |= NSEventModifierFlagCommand;
|
||||
return ans;
|
||||
}
|
||||
|
||||
// Rebuild a key event carrying the remapped flags, for handing to
|
||||
// interpretKeyEvents:. NOTE the characters strings were computed by the OS from
|
||||
// the PHYSICAL flags and cannot be recomputed here, so this path is only
|
||||
// approximately corrected; the UCKeyTranslate() path above, which is the one
|
||||
// normally taken, uses the remapped flags directly and is exact.
|
||||
static NSEvent*
|
||||
event_with_remapped_flags(NSEvent *event, NSUInteger flags) {
|
||||
if (flags == [event modifierFlags]) return event;
|
||||
NSEvent *ans = [NSEvent keyEventWithType:[event type]
|
||||
location:[event locationInWindow]
|
||||
modifierFlags:flags
|
||||
timestamp:[event timestamp]
|
||||
windowNumber:[event windowNumber]
|
||||
context:nil
|
||||
characters:[event characters]
|
||||
charactersIgnoringModifiers:[event charactersIgnoringModifiers]
|
||||
isARepeat:[event isARepeat]
|
||||
keyCode:[event keyCode]];
|
||||
return ans ? ans : event;
|
||||
}
|
||||
|
||||
static const char*
|
||||
format_mods(int mods) {
|
||||
static char buf[128];
|
||||
|
|
@ -1279,7 +1331,8 @@ is_ascii_control_char(char x) {
|
|||
}
|
||||
|
||||
const unsigned int keycode = [event keyCode];
|
||||
const NSUInteger flags = [event modifierFlags];
|
||||
const NSUInteger flags = remap_cocoa_flags([event modifierFlags]);
|
||||
event = event_with_remapped_flags(event, flags);
|
||||
const int mods = translateFlags(flags);
|
||||
const uint32_t key = translateKey(keycode, true);
|
||||
const bool process_text = !_glfw.ignoreOSKeyboardProcessing && (!window->ns.textInputFilterCallback || window->ns.textInputFilterCallback(key, mods, keycode, flags) != 1);
|
||||
|
|
@ -1383,7 +1436,7 @@ is_modifier_pressed(NSUInteger flags, NSUInteger target_mask, NSUInteger other_m
|
|||
int action = GLFW_RELEASE;
|
||||
const char old_first_char = _glfw.ns.text[0];
|
||||
_glfw.ns.text[0] = 0;
|
||||
const NSUInteger modifierFlags = [event modifierFlags] & NSEventModifierFlagDeviceIndependentFlagsMask;
|
||||
const NSUInteger modifierFlags = remap_cocoa_flags([event modifierFlags]) & NSEventModifierFlagDeviceIndependentFlagsMask;
|
||||
const uint32_t key = vk_code_to_functional_key_code([event keyCode]);
|
||||
const unsigned int keycode = [event keyCode];
|
||||
const int mods = translateFlags(modifierFlags);
|
||||
|
|
@ -1432,7 +1485,7 @@ is_modifier_pressed(NSUInteger flags, NSUInteger target_mask, NSUInteger other_m
|
|||
{
|
||||
const uint32_t keycode = [event keyCode];
|
||||
const uint32_t key = translateKey(keycode, true);
|
||||
const int mods = translateFlags([event modifierFlags]);
|
||||
const int mods = translateFlags(remap_cocoa_flags([event modifierFlags]));
|
||||
|
||||
GLFWkeyevent glfw_keyevent = {.key = key, .native_key = keycode, .native_key_id = keycode, .action = GLFW_RELEASE, .mods = mods};
|
||||
add_alternate_keys(&glfw_keyevent, event);
|
||||
|
|
|
|||
1
glfw/glfw3.h
vendored
1
glfw/glfw3.h
vendored
|
|
@ -4375,6 +4375,7 @@ GLFWAPI void glfwPostEmptyEvent(void);
|
|||
|
||||
GLFWAPI bool glfwGetIgnoreOSKeyboardProcessing(void);
|
||||
GLFWAPI void glfwSetIgnoreOSKeyboardProcessing(bool enabled);
|
||||
GLFWAPI void glfwSetModifierRemap(const int *table);
|
||||
GLFWAPI bool glfwGrabKeyboard(int grab);
|
||||
GLFWAPI void glfwGetKeyboardRepeatDelay(monotonic_t *delay, monotonic_t *interval);
|
||||
|
||||
|
|
|
|||
21
glfw/input.c
vendored
21
glfw/input.c
vendored
|
|
@ -746,6 +746,27 @@ GLFWAPI void glfwSetIgnoreOSKeyboardProcessing(bool enabled) {
|
|||
_glfw.ignoreOSKeyboardProcessing = enabled;
|
||||
}
|
||||
|
||||
// Apply the remap_modifier permutation to a GLFW modifier mask. Every source bit
|
||||
// is translated in the same pass, so a pair of declarations exchanging two
|
||||
// modifiers is a swap rather than the identity.
|
||||
int _glfwApplyModifierRemap(int mods) {
|
||||
if (!(mods & _glfw.modifierRemapMask)) return mods;
|
||||
int ans = 0;
|
||||
for (unsigned i = 0; i < GLFW_MOD_REMAP_SZ; i++) {
|
||||
const int bit = 1 << i;
|
||||
if (mods & bit) ans |= _glfw.modifierRemap[i] ? _glfw.modifierRemap[i] : bit;
|
||||
}
|
||||
return ans;
|
||||
}
|
||||
|
||||
GLFWAPI void glfwSetModifierRemap(const int *table) {
|
||||
_glfw.modifierRemapMask = 0;
|
||||
for (unsigned i = 0; i < GLFW_MOD_REMAP_SZ; i++) {
|
||||
_glfw.modifierRemap[i] = table ? table[i] : 0;
|
||||
if (_glfw.modifierRemap[i]) _glfw.modifierRemapMask |= 1 << i;
|
||||
}
|
||||
}
|
||||
|
||||
GLFWAPI bool glfwGrabKeyboard(int grab) {
|
||||
if (grab == 0 || grab == 1) {
|
||||
if (_glfwPlatformGrabKeyboard(grab)) _glfw.keyboard_grabbed = grab;
|
||||
|
|
|
|||
7
glfw/internal.h
vendored
7
glfw/internal.h
vendored
|
|
@ -57,6 +57,7 @@
|
|||
|
||||
#define EGL_PRESENT_OPAQUE_EXT 0x31df
|
||||
|
||||
#define GLFW_MOD_REMAP_SZ 8
|
||||
#define _GLFW_INSERT_FIRST 0
|
||||
#define _GLFW_INSERT_LAST 1
|
||||
|
||||
|
|
@ -620,6 +621,10 @@ struct _GLFWlibrary
|
|||
_GLFWmutex errorLock;
|
||||
|
||||
bool ignoreOSKeyboardProcessing, keyboard_grabbed;
|
||||
// remap_modifier: modifierRemap[i] is the GLFW modifier mask that the
|
||||
// modifier at bit i is presented as, or 0 to leave it alone.
|
||||
int modifierRemap[GLFW_MOD_REMAP_SZ];
|
||||
int modifierRemapMask;
|
||||
|
||||
struct {
|
||||
bool available;
|
||||
|
|
@ -926,3 +931,5 @@ void _glfw_free_clipboard_data(_GLFWClipboardData *cd);
|
|||
#define debug_rendering(...) if (_glfw.hints.init.debugRendering) { timed_debug_print(__VA_ARGS__); }
|
||||
#define debug_input(...) if (_glfw.hints.init.debugKeyboard) { timed_debug_print(__VA_ARGS__); }
|
||||
#define safe_close(fd) do { errno = 0; close(fd); } while(errno == EINTR)
|
||||
|
||||
int _glfwApplyModifierRemap(int mods);
|
||||
|
|
|
|||
5
glfw/x11_window.c
vendored
5
glfw/x11_window.c
vendored
|
|
@ -240,7 +240,10 @@ static int translateState(int state)
|
|||
if (state & Mod2Mask)
|
||||
mods |= GLFW_MOD_NUM_LOCK;
|
||||
|
||||
return mods;
|
||||
// Only button/DND events reach here; key events get their modifiers from the
|
||||
// xkb state, which is remapped in glfw_xkb_update_modifiers(). Remap here too
|
||||
// so mouse_map and map agree on X11.
|
||||
return _glfwApplyModifierRemap(mods);
|
||||
}
|
||||
|
||||
// Sends an EWMH or ICCCM event to the window manager
|
||||
|
|
|
|||
45
glfw/xkb_glfw.c
vendored
45
glfw/xkb_glfw.c
vendored
|
|
@ -697,10 +697,55 @@ glfw_xkb_compile_keymap(_GLFWXKBData *xkb, const char *map_str) {
|
|||
return true;
|
||||
}
|
||||
|
||||
// Map a GLFW modifier bit to the xkb modifier index for this keymap.
|
||||
static xkb_mod_index_t
|
||||
xkb_idx_for_glfw_mod(_GLFWXKBData *xkb, int glfw_mod) {
|
||||
switch (glfw_mod) {
|
||||
case GLFW_MOD_SHIFT: return xkb->shiftIdx;
|
||||
case GLFW_MOD_CONTROL: return xkb->controlIdx;
|
||||
case GLFW_MOD_ALT: return xkb->altIdx;
|
||||
case GLFW_MOD_SUPER: return xkb->superIdx;
|
||||
case GLFW_MOD_HYPER: return xkb->hyperIdx;
|
||||
case GLFW_MOD_META: return xkb->metaIdx;
|
||||
default: return XKB_MOD_INVALID;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply remap_modifier to an xkb modifier mask, BEFORE the state is used to
|
||||
// produce keysyms and text. Doing it here rather than on the finished event is
|
||||
// what keeps the two consistent: a modifier that participates in text production
|
||||
// (shift always; others depending on the layout) would otherwise report one
|
||||
// modifier while carrying text generated by another.
|
||||
static xkb_mod_mask_t
|
||||
remap_xkb_mask(_GLFWXKBData *xkb, xkb_mod_mask_t mask) {
|
||||
if (!_glfw.modifierRemapMask) return mask;
|
||||
xkb_mod_mask_t consumed = 0, produced = 0;
|
||||
for (unsigned i = 0; i < GLFW_MOD_REMAP_SZ; i++) {
|
||||
const int dest = _glfw.modifierRemap[i];
|
||||
if (!dest) continue;
|
||||
const xkb_mod_index_t src_idx = xkb_idx_for_glfw_mod(xkb, 1 << i);
|
||||
if (src_idx == XKB_MOD_INVALID || !(mask & (1u << src_idx))) continue;
|
||||
consumed |= 1u << src_idx;
|
||||
for (unsigned j = 0; j < GLFW_MOD_REMAP_SZ; j++) {
|
||||
if (!(dest & (1 << j))) continue;
|
||||
const xkb_mod_index_t dst_idx = xkb_idx_for_glfw_mod(xkb, 1 << j);
|
||||
if (dst_idx != XKB_MOD_INVALID) produced |= 1u << dst_idx;
|
||||
}
|
||||
}
|
||||
return (mask & ~consumed) | produced;
|
||||
}
|
||||
|
||||
void
|
||||
glfw_xkb_update_modifiers(_GLFWXKBData *xkb, xkb_mod_mask_t depressed, xkb_mod_mask_t latched, xkb_mod_mask_t locked, xkb_layout_index_t base_group, xkb_layout_index_t latched_group, xkb_layout_index_t locked_group) {
|
||||
if (!xkb->keymap) return;
|
||||
xkb->states.modifiers = 0;
|
||||
if (_glfw.modifierRemapMask) {
|
||||
const xkb_mod_mask_t before = depressed;
|
||||
depressed = remap_xkb_mask(xkb, depressed);
|
||||
latched = remap_xkb_mask(xkb, latched);
|
||||
locked = remap_xkb_mask(xkb, locked);
|
||||
if (before != depressed) debug("remap_modifier: xkb depressed mask 0x%x -> 0x%x\n", before, depressed);
|
||||
}
|
||||
xkb_state_update_mask(xkb->states.state, depressed, latched, locked, base_group, latched_group, locked_group);
|
||||
// We have to update the groups in clean_state, as they change for
|
||||
// different keyboard layouts, see https://github.com/kovidgoyal/kitty/issues/488
|
||||
|
|
|
|||
3
kitty/glfw-wrapper.c
generated
3
kitty/glfw-wrapper.c
generated
|
|
@ -293,6 +293,9 @@ load_glfw(const char* path) {
|
|||
*(void **) (&glfwSetIgnoreOSKeyboardProcessing_impl) = dlsym(handle, "glfwSetIgnoreOSKeyboardProcessing");
|
||||
if (glfwSetIgnoreOSKeyboardProcessing_impl == NULL) fail("Failed to load glfw function glfwSetIgnoreOSKeyboardProcessing with error: %s", dlerror());
|
||||
|
||||
*(void **) (&glfwSetModifierRemap_impl) = dlsym(handle, "glfwSetModifierRemap");
|
||||
if (glfwSetModifierRemap_impl == NULL) fail("Failed to load glfw function glfwSetModifierRemap with error: %s", dlerror());
|
||||
|
||||
*(void **) (&glfwGrabKeyboard_impl) = dlsym(handle, "glfwGrabKeyboard");
|
||||
if (glfwGrabKeyboard_impl == NULL) fail("Failed to load glfw function glfwGrabKeyboard with error: %s", dlerror());
|
||||
|
||||
|
|
|
|||
4
kitty/glfw-wrapper.h
generated
4
kitty/glfw-wrapper.h
generated
|
|
@ -2208,6 +2208,10 @@ typedef void (*glfwSetIgnoreOSKeyboardProcessing_func)(bool);
|
|||
GFW_EXTERN glfwSetIgnoreOSKeyboardProcessing_func glfwSetIgnoreOSKeyboardProcessing_impl;
|
||||
#define glfwSetIgnoreOSKeyboardProcessing glfwSetIgnoreOSKeyboardProcessing_impl
|
||||
|
||||
typedef void (*glfwSetModifierRemap_func)(const int*);
|
||||
GFW_EXTERN glfwSetModifierRemap_func glfwSetModifierRemap_impl;
|
||||
#define glfwSetModifierRemap glfwSetModifierRemap_impl
|
||||
|
||||
typedef bool (*glfwGrabKeyboard_func)(int);
|
||||
GFW_EXTERN glfwGrabKeyboard_func glfwGrabKeyboard_impl;
|
||||
#define glfwGrabKeyboard glfwGrabKeyboard_impl
|
||||
|
|
|
|||
96
kitty/glfw.c
96
kitty/glfw.c
|
|
@ -17,6 +17,8 @@
|
|||
#else
|
||||
#include "freetype_render_ui_text.h"
|
||||
#endif
|
||||
|
||||
static bool glfw_is_loaded = false;
|
||||
#define debug debug_rendering
|
||||
|
||||
typedef struct mouse_cursor {
|
||||
|
|
@ -462,11 +464,6 @@ refresh_callback(GLFWwindow *w) {
|
|||
request_tick_callback();
|
||||
}
|
||||
|
||||
#ifndef __APPLE__
|
||||
typedef struct modifier_key_state {
|
||||
bool left, right;
|
||||
} modifier_key_state;
|
||||
|
||||
static int
|
||||
key_to_modifier(uint32_t key, bool *is_left) {
|
||||
*is_left = false;
|
||||
|
|
@ -494,6 +491,51 @@ key_to_modifier(uint32_t key, bool *is_left) {
|
|||
}
|
||||
}
|
||||
|
||||
// The inverse of key_to_modifier(), used to keep a modifier key's own identity
|
||||
// consistent with its remapped modifier bit.
|
||||
static uint32_t
|
||||
modifier_to_key(int modifier, bool is_left) {
|
||||
switch (modifier) {
|
||||
case GLFW_MOD_SHIFT: return is_left ? GLFW_FKEY_LEFT_SHIFT : GLFW_FKEY_RIGHT_SHIFT;
|
||||
case GLFW_MOD_CONTROL: return is_left ? GLFW_FKEY_LEFT_CONTROL : GLFW_FKEY_RIGHT_CONTROL;
|
||||
case GLFW_MOD_ALT: return is_left ? GLFW_FKEY_LEFT_ALT : GLFW_FKEY_RIGHT_ALT;
|
||||
case GLFW_MOD_SUPER: return is_left ? GLFW_FKEY_LEFT_SUPER : GLFW_FKEY_RIGHT_SUPER;
|
||||
case GLFW_MOD_HYPER: return is_left ? GLFW_FKEY_LEFT_HYPER : GLFW_FKEY_RIGHT_HYPER;
|
||||
case GLFW_MOD_META: return is_left ? GLFW_FKEY_LEFT_META : GLFW_FKEY_RIGHT_META;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static_assert(GLFW_MOD_LAST < (1 << arraysz(((Options*)NULL)->modifier_remap)),
|
||||
"Options.modifier_remap is indexed by modifier bit position, it must cover every GLFW modifier");
|
||||
|
||||
#ifdef __APPLE__
|
||||
// Map a modifier named in kitty.conf back to the physical modifier that now
|
||||
// produces it (for the macOS menu bar). Unlike apply_modifier_remap() this is not
|
||||
// always possible, so it reports failure rather than guessing: false when more
|
||||
// than one source maps onto these modifiers, or when a requested modifier has
|
||||
// been remapped away so nothing physical produces it any more.
|
||||
static bool
|
||||
invert_modifier_remap(int mods, int *ans) {
|
||||
if (!OPT(modifier_remap_mask)) { *ans = mods; return true; }
|
||||
int consumed = 0, produced = 0;
|
||||
for (unsigned i = 0; i < arraysz(OPT(modifier_remap)); i++) {
|
||||
const int dest = OPT(modifier_remap)[i];
|
||||
if (!dest || (mods & dest) != dest) continue;
|
||||
if (consumed & dest) return false;
|
||||
consumed |= dest; produced |= 1 << i;
|
||||
}
|
||||
const int leftover = mods & ~consumed;
|
||||
if (leftover & OPT(modifier_remap_mask)) return false;
|
||||
*ans = leftover | produced;
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifndef __APPLE__
|
||||
typedef struct modifier_key_state {
|
||||
bool left, right;
|
||||
} modifier_key_state;
|
||||
|
||||
static void
|
||||
update_modifier_state_on_modifier_key_event(GLFWkeyevent *ev, int key_modifier, bool is_left) {
|
||||
|
|
@ -525,11 +567,31 @@ key_callback(GLFWwindow *w, GLFWkeyevent *ev) {
|
|||
#ifdef __APPLE__
|
||||
cocoa_clear_dock_badge_if_set();
|
||||
#endif
|
||||
#ifndef __APPLE__
|
||||
bool is_left;
|
||||
int key_modifier = key_to_modifier(ev->key, &is_left);
|
||||
// remap_modifier is applied to the modifier bits inside glfw, before the
|
||||
// platform produces the keysym and text. What is left to do here is a
|
||||
// modifier key's own identity, so that a program reading the keyboard
|
||||
// protocol does not see a self-contradictory event such as ctrl+LEFT_HYPER.
|
||||
//
|
||||
// This has to happen BEFORE the state fixup below, and key_modifier has to
|
||||
// become the remapped modifier: the event's mods already carry the remapped
|
||||
// bit, so adding the physical one there would report the physical modifier
|
||||
// on press and the remapped one on release.
|
||||
if (key_modifier > 0 && (OPT(modifier_remap_mask) & key_modifier)) {
|
||||
const int dest = OPT(modifier_remap)[__builtin_ctz((unsigned)key_modifier)];
|
||||
if (dest && !(dest & (dest - 1))) { // only when the destination is a single modifier
|
||||
const uint32_t remapped_key = modifier_to_key(dest, is_left);
|
||||
if (remapped_key) {
|
||||
debug_input("\x1b[35mremap_modifier\x1b[m: modifier key 0x%x -> 0x%x\n", ev->key, remapped_key);
|
||||
ev->key = remapped_key;
|
||||
key_modifier = dest;
|
||||
}
|
||||
}
|
||||
}
|
||||
#ifndef __APPLE__
|
||||
if (key_modifier != -1) update_modifier_state_on_modifier_key_event(ev, key_modifier, is_left);
|
||||
#endif
|
||||
#endif
|
||||
global_state.mods_at_last_key_or_button_event = ev->mods;
|
||||
global_state.callback_os_window->cursor_blink_zero_time = monotonic();
|
||||
if (is_window_ready_for_callbacks() && !ev->fake_event_on_focus_change) on_key_input(ev);
|
||||
|
|
@ -2150,6 +2212,7 @@ glfw_init(PyObject UNUSED *self, PyObject *args) {
|
|||
cocoa_set_uncaught_exception_handler();
|
||||
#endif
|
||||
const char* err = load_glfw(path);
|
||||
if (!err) { glfw_is_loaded = true; push_modifier_remap_to_glfw(); }
|
||||
if (err) { PyErr_SetString(PyExc_RuntimeError, err); return NULL; }
|
||||
glfwSetErrorCallback(error_callback);
|
||||
glfwInitHint(GLFW_DEBUG_KEYBOARD, debug_keyboard);
|
||||
|
|
@ -2794,7 +2857,17 @@ set_custom_cursor(PyObject *self UNUSED, PyObject *args) {
|
|||
void
|
||||
get_cocoa_key_equivalent(uint32_t key, int mods, char *cocoa_key, size_t key_sz, int *cocoa_mods) {
|
||||
memset(cocoa_key, 0, key_sz);
|
||||
uint32_t ans = glfwGetCocoaKeyEquivalent(key, mods, cocoa_mods);
|
||||
// Menu accelerators come from kitty.conf, i.e. they are named in post-remap
|
||||
// terms. Invert the remap so the menu bar displays and triggers on the
|
||||
// physical key that now produces that modifier. Leaving cocoa_key empty
|
||||
// makes the caller skip this accelerator, which is what we want whenever the
|
||||
// physical combination cannot be named: Cocoa has no flag for hyper or meta,
|
||||
// so emitting one would silently produce a key equivalent with NO modifiers
|
||||
// at all - e.g. a bare "q" that quits kitty.
|
||||
int physical_mods;
|
||||
if (!invert_modifier_remap(mods, &physical_mods)) return;
|
||||
if (physical_mods & ~(GLFW_MOD_SHIFT | GLFW_MOD_CONTROL | GLFW_MOD_ALT | GLFW_MOD_SUPER | GLFW_MOD_CAPS_LOCK)) return;
|
||||
uint32_t ans = glfwGetCocoaKeyEquivalent(key, physical_mods, cocoa_mods);
|
||||
if (ans) encode_utf8(ans, cocoa_key);
|
||||
}
|
||||
|
||||
|
|
@ -2959,6 +3032,13 @@ set_ignore_os_keyboard_processing(bool enabled) {
|
|||
glfwSetIgnoreOSKeyboardProcessing(enabled);
|
||||
}
|
||||
|
||||
// set_options() runs before glfw is loaded at startup, and again on every config
|
||||
// reload, so push the table from both places and no-op until glfw exists.
|
||||
void
|
||||
push_modifier_remap_to_glfw(void) {
|
||||
if (glfw_is_loaded) glfwSetModifierRemap(OPT(modifier_remap));
|
||||
}
|
||||
|
||||
static void
|
||||
decref_pyobj(void *x) {
|
||||
Py_XDECREF(x);
|
||||
|
|
|
|||
|
|
@ -3532,6 +3532,74 @@ this option to alter all default shortcuts that use :opt:`kitty_mod`.
|
|||
""",
|
||||
)
|
||||
|
||||
opt(
|
||||
'+remap_modifier',
|
||||
'ctrl super',
|
||||
option_type='remap_modifier',
|
||||
ctype='!remap_modifier',
|
||||
add_to_default=False,
|
||||
long_text="""
|
||||
Present one modifier key to kitty as if it were another. The syntax is::
|
||||
|
||||
remap_modifier <from> <to>
|
||||
|
||||
for example :code:`remap_modifier ctrl super` makes every :kbd:`Ctrl+key` press
|
||||
arrive as :kbd:`Super+key`. Only :code:`shift`, :code:`alt`, :code:`ctrl`,
|
||||
:code:`super`, :code:`hyper` and :code:`meta` can be named. The source must be
|
||||
exactly one modifier; the destination may name more than one, in which case
|
||||
holding the source is indistinguishable from holding all of them.
|
||||
|
||||
This option can be specified multiple times, and the declarations are applied as
|
||||
a single simultaneous permutation, once, as each event arrives. So::
|
||||
|
||||
remap_modifier ctrl super
|
||||
remap_modifier super ctrl
|
||||
|
||||
exchanges the two rather than collapsing to no change. Ordering among
|
||||
:opt:`remap_modifier` declarations matters only in that the last declaration for
|
||||
a given source wins; where they sit relative to other options in
|
||||
:file:`kitty.conf` makes no difference.
|
||||
|
||||
The remapping happens before anything else looks at the event, so it is
|
||||
application wide: kitty's own keyboard shortcuts, :opt:`kitty_mod`,
|
||||
:opt:`mouse_map` and the keys sent to the program running in the terminal all
|
||||
see the remapped modifier. Every mapping in :file:`kitty.conf` is therefore
|
||||
written in terms of the modifier a key *becomes*, not the one printed on the
|
||||
keycap — including the built-in shortcuts, so :code:`remap_modifier ctrl super`
|
||||
moves every default :code:`ctrl+shift+…` binding onto the physical Super key
|
||||
unless you also change :opt:`kitty_mod`.
|
||||
|
||||
A modifier key's own press and release are remapped too, so that a program using
|
||||
the full keyboard protocol does not see, for example, ctrl reported together with
|
||||
the Hyper key. That is only possible when the destination is a single modifier;
|
||||
with a multi-modifier destination the key itself keeps its original identity.
|
||||
|
||||
On macOS, a menu bar accelerator is shown on the physical key that now produces
|
||||
the modifier it was declared with. Where no such key exists — because more than
|
||||
one modifier maps onto it, or because it maps to hyper or meta, which the macOS
|
||||
menu bar cannot express — the accelerator is left off the menu rather than shown
|
||||
incorrectly. The shortcut itself continues to work.
|
||||
|
||||
On macOS the remap is applied before the key event reaches the text input
|
||||
system, so :kbd:`Option`, which produces text natively unless
|
||||
:opt:`macos_option_as_alt` is set, behaves consistently with the modifiers it
|
||||
reports. A destination of :kbd:`hyper` or :kbd:`meta` cannot be expressed in
|
||||
Cocoa's modifier flags, so such a remap is left unapplied there rather than
|
||||
silently losing the modifier.
|
||||
|
||||
On Wayland, kitty only detects the :kbd:`hyper` and :kbd:`meta` modifiers when
|
||||
:envvar:`KITTY_WAYLAND_DETECT_MODIFIERS` is set in the environment. Without it
|
||||
those two never appear on key events at all, so remapping to or from them has
|
||||
nothing to act on there. This is pre-existing behaviour, not a limitation of
|
||||
this option.
|
||||
|
||||
This is useful for keyboard layouts that move Control somewhere more comfortable
|
||||
— for example placing a Hyper key on Caps Lock to use for readline and TUI
|
||||
editing, while leaving the physical Control key free for GUI-style shortcuts.
|
||||
Use :code:`kitty --debug-input` to see the remapping applied to each event.
|
||||
""",
|
||||
)
|
||||
|
||||
opt(
|
||||
'clear_all_shortcuts',
|
||||
'no',
|
||||
|
|
|
|||
22
kitty/options/parse.py
generated
22
kitty/options/parse.py
generated
|
|
@ -17,14 +17,15 @@ from kitty.options.utils import (
|
|||
deprecated_send_text, disable_ligatures, edge_width, env, filter_notification, font_features,
|
||||
hide_window_decorations, macos_option_as_alt, macos_titlebar_color, menu_map, modify_font,
|
||||
mouse_hide_wait, narrow_symbols, notify_on_cmd_finish, optional_edge_width, parse_font_spec,
|
||||
parse_map, parse_mouse_map, paste_actions, pointer_shape_when_dragging, remote_control_password,
|
||||
resize_debounce_time, scrollback_lines, scrollback_pager_history_size, scrollbar_color,
|
||||
shell_integration, show_hyperlink_targets, store_multiple, symbol_map, tab_activity_symbol,
|
||||
tab_bar_edge, tab_bar_margin_height, tab_bar_min_tabs, tab_fade, tab_font_style, tab_separator,
|
||||
tab_title_template, tab_title_wrap, text_fg_override_threshold, titlebar_color, to_cursor_shape,
|
||||
to_cursor_unfocused_shape, to_font_size, to_layout_names, to_modifiers,
|
||||
transparent_background_colors, underline_exclusion, url_prefixes, url_style, visual_bell_duration,
|
||||
visual_window_select_characters, window_border_width, window_logo_scale, window_size
|
||||
parse_map, parse_mouse_map, paste_actions, pointer_shape_when_dragging, remap_modifier,
|
||||
remote_control_password, resize_debounce_time, scrollback_lines, scrollback_pager_history_size,
|
||||
scrollbar_color, shell_integration, show_hyperlink_targets, store_multiple, symbol_map,
|
||||
tab_activity_symbol, tab_bar_edge, tab_bar_margin_height, tab_bar_min_tabs, tab_fade,
|
||||
tab_font_style, tab_separator, tab_title_template, tab_title_wrap, text_fg_override_threshold,
|
||||
titlebar_color, to_cursor_shape, to_cursor_unfocused_shape, to_font_size, to_layout_names,
|
||||
to_modifiers, transparent_background_colors, underline_exclusion, url_prefixes, url_style,
|
||||
visual_bell_duration, visual_window_select_characters, window_border_width, window_logo_scale,
|
||||
window_size
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -1230,6 +1231,10 @@ class Parser:
|
|||
|
||||
choices_for_progress_bar = frozenset(('left', 'right', 'top', 'bottom', 'hidden'))
|
||||
|
||||
def remap_modifier(self, val: str, ans: dict[str, typing.Any]) -> None:
|
||||
for k, v in remap_modifier(val):
|
||||
ans["remap_modifier"][k] = v
|
||||
|
||||
def remember_window_position(self, val: str, ans: dict[str, typing.Any]) -> None:
|
||||
ans['remember_window_position'] = to_bool(val)
|
||||
|
||||
|
|
@ -1628,6 +1633,7 @@ def create_result_dict() -> dict[str, typing.Any]:
|
|||
'menu_map': {},
|
||||
'modify_font': {},
|
||||
'narrow_symbols': {},
|
||||
'remap_modifier': {},
|
||||
'remote_control_password': {},
|
||||
'symbol_map': {},
|
||||
'watcher': {},
|
||||
|
|
|
|||
15
kitty/options/to-c-generated.h
generated
15
kitty/options/to-c-generated.h
generated
|
|
@ -1526,6 +1526,19 @@ convert_from_opts_wayland_enable_ime(PyObject *py_opts, Options *opts) {
|
|||
Py_DECREF(ret);
|
||||
}
|
||||
|
||||
static void
|
||||
convert_from_python_remap_modifier(PyObject *val, Options *opts) {
|
||||
remap_modifier(val, opts);
|
||||
}
|
||||
|
||||
static void
|
||||
convert_from_opts_remap_modifier(PyObject *py_opts, Options *opts) {
|
||||
PyObject *ret = PyObject_GetAttrString(py_opts, "remap_modifier");
|
||||
if (ret == NULL) return;
|
||||
convert_from_python_remap_modifier(ret, opts);
|
||||
Py_DECREF(ret);
|
||||
}
|
||||
|
||||
static bool
|
||||
convert_opts_from_python_opts(PyObject *py_opts, Options *opts) {
|
||||
convert_from_opts_font_size(py_opts, opts);
|
||||
|
|
@ -1762,5 +1775,7 @@ convert_opts_from_python_opts(PyObject *py_opts, Options *opts) {
|
|||
if (PyErr_Occurred()) return false;
|
||||
convert_from_opts_wayland_enable_ime(py_opts, opts);
|
||||
if (PyErr_Occurred()) return false;
|
||||
convert_from_opts_remap_modifier(py_opts, opts);
|
||||
if (PyErr_Occurred()) return false;
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -500,6 +500,25 @@ box_drawing_scale(PyObject *val, Options *opts) {
|
|||
}
|
||||
}
|
||||
|
||||
static inline void
|
||||
remap_modifier(PyObject *val, Options *opts) {
|
||||
memset(opts->modifier_remap, 0, sizeof(opts->modifier_remap));
|
||||
opts->modifier_remap_mask = 0;
|
||||
if (!PyDict_Check(val)) { PyErr_SetString(PyExc_TypeError, "remap_modifier must be a dict"); return; }
|
||||
PyObject *key, *value;
|
||||
Py_ssize_t pos = 0;
|
||||
while (PyDict_Next(val, &pos, &key, &value)) {
|
||||
const long src = PyLong_AsLong(key), dest = PyLong_AsLong(value);
|
||||
if (PyErr_Occurred()) { PyErr_Print(); PyErr_Clear(); continue; }
|
||||
// the parser guarantees a single source bit, but do not trust it in C
|
||||
if (src <= 0 || (src & (src - 1)) || dest <= 0) continue;
|
||||
const unsigned idx = (unsigned)__builtin_ctz((unsigned)src);
|
||||
if (idx >= arraysz(opts->modifier_remap)) continue;
|
||||
opts->modifier_remap[idx] = (int)dest;
|
||||
opts->modifier_remap_mask |= (int)src;
|
||||
}
|
||||
}
|
||||
|
||||
static inline void
|
||||
text_composition_strategy(PyObject *val, Options *opts) {
|
||||
if (!PyUnicode_Check(val)) { PyErr_SetString(PyExc_TypeError, "text_composition_strategy must be a string"); return; }
|
||||
|
|
|
|||
3
kitty/options/types.py
generated
3
kitty/options/types.py
generated
|
|
@ -423,6 +423,7 @@ option_names = (
|
|||
'pointer_shape_when_dragging',
|
||||
'pointer_shape_when_grabbed',
|
||||
'progress_bar',
|
||||
'remap_modifier',
|
||||
'remember_window_position',
|
||||
'remember_window_size',
|
||||
'remote_control_password',
|
||||
|
|
@ -739,6 +740,7 @@ class Options:
|
|||
menu_map: dict[tuple[str, ...], str] = {}
|
||||
modify_font: dict[str, kitty.fonts.FontModification] = {}
|
||||
narrow_symbols: dict[tuple[int, int], int] = {}
|
||||
remap_modifier: dict[int, int] = {}
|
||||
remote_control_password: dict[str, collections.abc.Sequence[str]] = {}
|
||||
symbol_map: dict[tuple[int, int], str] = {}
|
||||
watcher: dict[str, str] = {}
|
||||
|
|
@ -868,6 +870,7 @@ defaults.kitten_alias = {}
|
|||
defaults.menu_map = {}
|
||||
defaults.modify_font = {}
|
||||
defaults.narrow_symbols = {}
|
||||
defaults.remap_modifier = {}
|
||||
defaults.remote_control_password = {}
|
||||
defaults.symbol_map = {}
|
||||
defaults.watcher = {}
|
||||
|
|
|
|||
|
|
@ -487,6 +487,47 @@ def to_modifiers(val: str) -> int:
|
|||
return parse_mods(val.split('+'), val) or 0
|
||||
|
||||
|
||||
def remap_modifier(val: str) -> Iterable[tuple[int, int]]:
|
||||
# Only the real modifiers may be remapped. parse_mods() also accepts
|
||||
# kitty_mod (a parse-time placeholder resolved out of keymaps before any key
|
||||
# event exists) and the lock modifiers, neither of which can be meaningfully
|
||||
# carried on an event, so they are rejected here rather than silently
|
||||
# mangling events later.
|
||||
remappable = (
|
||||
defines.GLFW_MOD_SHIFT | defines.GLFW_MOD_ALT | defines.GLFW_MOD_CONTROL |
|
||||
defines.GLFW_MOD_SUPER | defines.GLFW_MOD_HYPER | defines.GLFW_MOD_META
|
||||
)
|
||||
|
||||
def bad(msg: str) -> None:
|
||||
log_error(f'Ignoring invalid remap_modifier "{val}": {msg}')
|
||||
|
||||
parts = val.split()
|
||||
if len(parts) != 2:
|
||||
bad('must be two modifier names separated by whitespace')
|
||||
return
|
||||
# parse_mods() returns None both for an unknown name (already logged) and for
|
||||
# "none" (deliberately not logged), so check the names before trusting it
|
||||
for part in parts:
|
||||
for name in part.split('+'):
|
||||
if name.upper() in ('NONE', ''):
|
||||
bad('none is not a modifier')
|
||||
return
|
||||
src = parse_mods(parts[0].split('+'), val)
|
||||
dest = parse_mods(parts[1].split('+'), val)
|
||||
if src is None or dest is None:
|
||||
return # parse_mods() has already logged the unknown modifier name
|
||||
if src & ~remappable or dest & ~remappable:
|
||||
bad('only shift, alt, ctrl, super, hyper and meta can be remapped')
|
||||
return
|
||||
if src & (src - 1):
|
||||
bad('the source must name exactly one modifier')
|
||||
return
|
||||
if src == dest:
|
||||
bad('the source and destination are the same')
|
||||
return
|
||||
yield src, dest
|
||||
|
||||
|
||||
def parse_shortcut(sc: str) -> SingleKey:
|
||||
if sc.endswith('+') and len(sc) > 1:
|
||||
sc = f'{sc[:-1]}plus'
|
||||
|
|
|
|||
|
|
@ -957,6 +957,7 @@ PYWRAP1(set_options) {
|
|||
global_state.debug_rendering = debug_rendering ? true : false;
|
||||
global_state.debug_font_fallback = debug_font_fallback ? true : false;
|
||||
if (!convert_opts_from_python_opts(opts, &global_state.opts)) return NULL;
|
||||
push_modifier_remap_to_glfw();
|
||||
Py_XDECREF(global_state.options_object);
|
||||
global_state.options_object = Py_NewRef(opts);
|
||||
Py_RETURN_NONE;
|
||||
|
|
|
|||
|
|
@ -117,6 +117,11 @@ typedef struct Options {
|
|||
bool resize_in_steps;
|
||||
bool sync_to_monitor;
|
||||
bool close_on_child_death;
|
||||
// remap_modifier: modifier_remap[i] is the mask that the modifier at bit i
|
||||
// is presented as, or 0 to leave it alone. modifier_remap_mask is the OR of
|
||||
// all remapped source bits, used as a fast path.
|
||||
int modifier_remap[8];
|
||||
int modifier_remap_mask;
|
||||
bool window_alert_on_bell;
|
||||
bool macos_dock_badge_on_bell;
|
||||
bool debug_keyboard;
|
||||
|
|
@ -626,6 +631,7 @@ bool mouse_select_cmd_output(Window *w);
|
|||
bool move_cursor_to_mouse_if_at_shell_prompt(Window *w);
|
||||
void mouse_selection(Window *w, int code, int button);
|
||||
const char* format_mods(unsigned mods);
|
||||
|
||||
void dispatch_pending_clicks(id_type, void*);
|
||||
void send_pending_click_to_window(Window*, int);
|
||||
void get_platform_dependent_config_values(void *glfw_window);
|
||||
|
|
@ -636,6 +642,7 @@ void update_ime_focus(OSWindow* osw, bool focused);
|
|||
void update_ime_position(Window* w, Screen *screen);
|
||||
bool update_ime_position_for_window(id_type window_id, bool force, int update_focus);
|
||||
void set_ignore_os_keyboard_processing(bool enabled);
|
||||
void push_modifier_remap_to_glfw(void);
|
||||
void update_menu_bar_title(PyObject *title UNUSED);
|
||||
void change_live_resize_state(OSWindow*, bool);
|
||||
bool render_os_window(OSWindow *w, monotonic_t now, bool scan_for_animated_images);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue