tiling window managers are wonderful. ultra-flexible text editors are also wonderful. for a spell, i thought i'd found the ideal solution in exwm … and i think it would have been, save for the fact that i use ordinary, graphical windows as much, if not more than, text buffers, and sometimes those windows are from dodgy programs (e.g., steam) that have trouble with EXWM's fancy input methods.
but i still like emacs a lot. hell, it switches light and dark mode on my machine (still)! so, inspired by such posts as \(\sqrt{-1}\)'s, i set out to get a common set of keybindings between emacs and i3, along with some sane defaults around opening terminals, splitting windows, etc.
i first tried a script with xdotool and emacsclient , as in the above-referenced article, and that worked… but proved to be too slow: i saw lags of up to a second timing the script gave a latency of 30 to 100 ms from invocation to exit, which is still pretty slow but not a dealbreaker. i still don't know where the rest of the latency came from. between sending input to emacs and it actually registering. i don't know if this is because of my emacs version, other packages, emacsclient weirdness, whatever, but that wasn't going to cut it. plus, it seems wasteful to launch a whole shell-plus just to register a keypress, especially for some of the most commonly pressed key combinations i use. so i did the only rational thing: i patched i3.
my objective was: instead of unilaterally handling commands bound via i3's bindsym , add an option to check the currently focused window to see if it's emacs, and if it is, pass the keypress event through to it. note that this feature has been requested in the past, and the i3 maintainers have deemed it to be out of scope. i would make this a more fully-fledged patch if that were not the case. if emacs decides "no, i3 should actually handle this," it can use i3-msg to route the action back.
i succeeded in that, though it might not be the most elegant thing in the world. if you know about xcb and want to give me advice, please! send me an email at [email protected].
relevant i3 code i3 uses xcb_grab_key() with owner_events = 0 on the root x window to intercept keys. the relevant code in src/bindings.c looks like all unpatched code snippets refer to i3 4.25.1, if you want to follow along. 172 struct Binding_Keycode * binding_keycode ; 173 TAILQ_FOREACH (binding_keycode, &(bind->keycodes_head), keycodes) { 174 const int keycode = binding_keycode->keycode; 175 const int mods = (binding_keycode->modifiers & 0xFFFF); 176 DLOG( "Binding %p Grabbing keycode %d with mods %d
" , bind, keycode, mods); 177 xcb_grab_key(conn, 0, root, mods, keycode, XCB_GRAB_MODE_ASYNC, 178 XCB_GRAB_MODE_ASYNC); 179 } this code isn't super relevant, except that i3 entirely steals its bindings from anyone else by intercepting on the root window. if you're thinking that setting owner_events = 1 to allow event passthrough so we don't have to re-emit… that would be great, but that appears to instruct x to pass the event through to only the root window. which is not what we want. in i3's handle_event() in src/handlers.c , if it gets an xcb event, it sends it off to a specialized handler based on its type: 1481 switch (type) { 1482 case XCB_KEY_PRESS: 1483 case XCB_KEY_RELEASE: 1484 handle_key_press(( xcb_key_press_event_t *)event); 1485 break ; 1486 } handle_key_press() ( src/key_press.c ) looks like this — it receives a keypress event, looks up a binding based on that event, and, if it finds one, runs the associated command: yes, i do know one of the lines is too long. i opted to leave it that way, as that's how it is in the i3 source. i should note, though: i3 has really nice source code! i found it very readable and pleasant to work inside. 12 18 void handle_key_press ( xcb_key_press_event_t * event ) { 19 const bool key_release = (event->response_type == XCB_KEY_RELEASE); 20 21 last_timestamp = event->time; 22 23 DLOG( "%s %d, state raw = 0x%x
" , (key_release ? "KeyRelease" : "KeyPress" ), event->detail, event->state); 24 25 Binding * bind = get_binding_from_xcb_event(( xcb_generic_event_t *)event); 26 27 28 if (bind == NULL ) { 29 return ; 30 } 31 32 CommandResult * result = run_binding(bind, NULL ); 33 command_result_free(result); 34 } notably, this function receives the original xcb_key_press_event_t from xcb , which (after a bit of reading and experimentation) i realized you could just re-emit diretly via xcb_send_event() . unfortunately, the window receiving the event will still lose focus, as i3 is intercepting key events globally. i haven't fixed this; let me know if you know how. this looks like a reasonable place to make a change!
the patch Binding struct changes i decided to modify Binding ( include/data.h ) with an extra field to indicate a class of window which should, for that binding, receive events directly: /** * Holds a keybinding, consisting of a keycode combined with modifiers and the * command which is executed as soon as the key is pressed (see * src/config_parser.c) * */ struct Binding { /** Window class to use for key passthrough. Currently an exact string match. */ struct { char * class ; } passthrough ; }; i also modified the binding initialization to set up passthrough, if provided: there is, of course, associated cleanup code, which i've omitted for brevity. look at the patch file (linked at the end) if you want to see it. Binding * configure_binding ( const char * bindtype , const char * modifiers , const char * input_code , const char * release , const char * border , const char * whole_window , const char * exclude_titlebar , const char * command , const char * modename , bool pango_markup , const char * passthrough ) { if (passthrough) { new_binding->passthrough.class = sstrdup( "Emacs" ); } else { new_binding->passthrough.class = NULL ; } return new_binding; } handle_key_press() now has to look at that setting and decide whether to pass the key event through. if bind->passthrough.class is set for that binding, we get the currently focused window, check its class, and if that class matches, we re-send the key event to that focused window with interception disabled (else it would just go straight back to i3): void handle_key_press ( xcb_key_press_event_t * event ) { DLOG( "PATCH: checking if we should pass keypress through
" ); if (bind->passthrough.class) { xcb_generic_error_t * focus_error ; xcb_get_input_focus_reply_t * input_focus = xcb_get_input_focus_reply( conn, xcb_get_input_focus(conn), &focus_error); if (focus_error != NULL ) { DLOG( "PATCH: could not get focused window" ); free(focus_error); } else { Con * con = con_by_window_id(input_focus->focus); const xcb_window_t focus = input_focus->focus; free(input_focus); const bool should_pass = con && con->window->class_class && strcmp(con->window->class_class, bind->passthrough.class) == 0; if (should_pass) { DLOG( "PATCH: forwarding keypress (%d %s %s @ %d %d)
... continue reading