My I3-Emacs Integration The article describes the author's attempt to integrate the i3 tiling window manager with the Emacs text editor by creating shared keybindings. After finding that using a script with xdotool and emacsclient was too slow (with latency up to a second), the author patched i3's source code to check if the focused window is Emacs and pass keypress events through to it when appropriate. The author notes that while this feature has been requested before, i3 maintainers considered it out of scope, so the patch remains a personal modification rather than an official addition. tiling window managers are wonderful. ultra-flexible text editors are also wonderful. for a spell, i thought i'd found the ideal solution in 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 https://sqrtminusone.xyz/posts/2021-10-04-emacs-i3/ , 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 https://github.com/i3/i3/issues/4768 , 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 web@khz.ac mailto:web@khz.ac . relevant i3 code i3 uses xcb grab key with owner events = 0 on the root src/bindings.c looks like all unpatched code snippets refer to i3 4.25.1, if you want to follow along. 172struct Binding Keycode binding keycode; 173TAILQ 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\n", 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 only the root window. which is not what we want. in i3's handle event in src/handlers.c , if it gets an 1481switch type { 1482case XCB KEY PRESS: 1483case XCB KEY RELEASE: 1484 handle key press xcb key press event t event ; 1485 break; 1486 // ... 1487} 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/ 13 There was a KeyPress or KeyRelease both events have the same fields . We 14 compare this key code with our bindings table and pass the bound action to 15 parse command . 16 17 / 18void 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\n", key release ? "KeyRelease" : "KeyPress" , event- detail, event- state ; 24 25 Binding bind = get binding from xcb event xcb generic event t event ; 26 27 / if we couldn't find a binding, we are done / 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 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. / Adds a binding from config parameters given as strings and returns a pointer to the binding structure. Returns NULL if the input code could not be parsed. / 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 { // ... // XXX: should change this to be configurable, but I only care about Emacs, so. 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\n" ; 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 \n", focus, con- name, con- window- class class, event- event x, event- event y ; event- event = focus; xcb send event conn, false, focus, XCB EVENT MASK NO EVENT, const char event ; return; } } } DLOG "PATCH: handling keypress normally\n" ; CommandResult result = run binding bind, NULL ; command result free result ; } modifying the parser i3 includes a parser generator, which reads what appears to be an i3-specific me to do it, email me. the parser configuration for bindsym / bindcode parser-specs/config.spec , after modification, looks like this: bindsym/bindcode state BINDING: ... passthrough = '--passthrough' - key = word - BINDCOMMAND state BINDCOMMAND: ... passthrough = '--passthrough' - command = string - call cfg binding ..., $passthrough, $command this section of the parser config defines two parser states: BINDING parsing a bindsym command, but we haven't parsed a keysym yet and BINDCOMMAND the same, but after we've parsed the keysym . the right way to do this, should i have wanted to have syntax like --passthrough "Emacs" , would be to move to a new parsing state upon encountering this flag and eating the next token as passthrough . perhaps someday. i3's parsing variable =