Skip to content
Chimera readability score 65 out of 100, Academic reading level.

tiling window managers are wonderful. ultra-flexible text editors are also

wonderful. for a spell, i thought i'd found the ideal solution in

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

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

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

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 = <stuff>

and passing that variable to a call

command as a char*

— non-null if encountered and null if not. hence, if the flag --passthrough

appears while parsing, $passthrough

evaluates to the string "--passthrough"

rather than NULL

. then if (passthrough) { / ... / }

gets evaluated in

configure_binding()

, and the rest is history.

the emacs side

now that key passthrough works, all we need is a bit of elisp and life is good. a lot of this is heavily pulled from the \(\sqrt{-1}\) post linked above. basically, i want to integrate two actions: window movement and opening terminals.

window movement

to start, we need a way for emacs to send messages back to i3 if we try to move beyond an existing window:

(defmacro nausicaa/i3-msg (&rest args) "Call i3-msg with ARGS." `(start-process "emacs-to-i3" nil "i3-msg" ,@args))

when either moving windows or moving between windows, emacs should attempt to select one of its own windows in the given direction. failing that, it should instruct i3 to do so:

(defun nausicaa/emacs-i3-windmove (dir) "Select window in DIR, if it exists; if not, i3-select it." (let ((other-window (nausicaa/find-other-window dir))) (if (or (null other-window) (window-minibuffer-p other-window)) (nausicaa/i3-msg "focus" (symbol-name dir)) (nausicaa/do-window-select dir)))) (defun nausicaa/emacs-i3-move-window (dir) "Do some stuff to move window in DIR. I should check out `evil-move-window' at some point." (let ((other-window (windmove-find-other-window dir))) (cond ((and other-window (not (window-minibuffer-p other-window))) (window-swap-states (selected-window) other-window)) (t (nausicaa/i3-msg "move" (symbol-name dir))))))

nausicaa/find-other-window

is a function that really just invokes the

appropriate windmove command. i wrote it because my existing windmove commands

have advice around them (placed there by doom, i expect) that allows them to

select popup windows and the minibuffer, which i wanted to reuse:

(defun nausicaa/find-other-window (&rest args) "Pass ARGS through to `windmove-find-other-window'. Exists solely so I can reuse `+popup--ignore-window-parameters-a'." (apply #'windmove-find-other-window args)) (defun nausicaa/do-window-select (&rest args) "Pass ARGS through to `windmove-do-window-select'. Exists solely so I can reuse `+popup--ignore-window-parameters-a'." (apply #'windmove-do-window-select args)) (advice-add 'nausicaa/find-other-window :around #'+popup--ignore-window-parameters-a) (advice-add 'nausicaa/do-window-select :around #'+popup--ignore-window-parameters-a)

arguably the right way to do this is to add that advice to

windmove-find-other-window

, which i might do at some point.

terminals

i am always launching terminals —

sometimes

fifty

a

day.

i typically use mistty as a terminal, since it has delightful integration with the rest of emacs, but it tends to choke on more difficult text rendering tasks, for which alacritty is better suited. at any given moment, in any given directory, i might want to launch either of them, so i wrote a few scripts to invoke either mistty or alacritty from either emacs or i3.

i3 is configured to launch both mistty and alacritty, depending on context, using two scripts:

start a terminal bindsym --passthrough $super+Return exec mistty-create bindsym --passthrough $super+Control+Return exec alacritty-create

if those keys pass through to emacs, emacs either launches a mistty session or just shells out to the script:

(defun nausicaa/launch-alacritty () (interactive) (async-start-process "alacritty-create" "bash" nil "-c" "exec alacritty-create")) (map! "s-<return>" #'mistty-create "C-s-<return>" #'nausicaa/launch-alacritty)

mistty-create

is a shell script that tells emacs to open a new frame with mistty in it:

pkgs.writeShellApplication { name = "mistty-create"; text = '' ${config.programs.emacs.package}/bin/emacsclient -e "(progn (other-frame-prefix) (mistty-create))" ''; }

alacritty-create

instructs the current alacritty process to create a new window

in the current working directory:

pkgs.writeShellApplication { name = "alacritty-create"; text = '' if ! ${pkgs.alacritty}/bin/alacritty msg create-window --working-directory "$PWD"; then env -u INSIDE_EMACS ${pkgs.alacritty}/bin/alacritty "$@" >/dev/null 2>&1 & disown %env fi ''; }

this script has the wonderful property that, if invoked inside emacs, you get an alacritty window in whatever project directory you're currently in, yielding roughly equally ergonomic behavior between mistty and alacritty. it's great.

results

i3 and emacs play really nicely together now. if you want the patch for i3, it's here. i'll eventually post my full configuration with keycodes, but the above should be enough to get something working.

and if you're like me, and want to do i3 development on nix, here's what i used

for a shell.nix

:

{ pkgs ? import <nixpkgs> { } }: pkgs.mkShell { nativeBuildInputs = with pkgs; [ pkg-config makeWrapper meson ninja installShellFiles perl asciidoc xmlto docbook_xml_dtd_45 docbook_xsl findXMLCatalogs ]; buildInputs = with pkgs.buildPackages; [ libxcb libxcb-util libxcb-wm libxcb-keysyms libxkbcommon xcbutilxrm libstartup_notification libx11 pcre2 libev yajl xcb-util-cursor perl pango perlPackages.AnyEventI3 perlPackages.X11XCB perlPackages.IPCRun perlPackages.ExtUtilsPkgConfig perlPackages.InlineC ]; }

Facts Only

The article introduces the Argumentative Resilience Codex (A.R.C.) for analyzing news and information
Three perspectives: RED (facts only), BLUE (balanced synthesis with context), PURPLE (pattern analysis and deeper implications)
A.R.C. aims to promote cognitive sovereignty and resist manipulation in various forms

Executive Summary

The article discusses the development of an A.R.C. (Argumentative Resilience Codex) for analyzing news and information, emphasizing cognitive sovereignty through a three-perspective approach: RED (facts only), BLUE (balanced synthesis with context), and PURPLE (pattern analysis and deeper implications). The primary focus is on fostering intellectual honesty and critical thinking skills to resist manipulation in various forms.
The A.R.C. system provides a structured approach for analyzing content, considering factors such as methodology, claims versus evidence, literature context, real-world implications, and bridge questions when dealing with academic and scholarly content. For news/media analysis, it detects manipulation patterns, identifies root causes, examines implications, poses bridge questions, and assesses counter-influence campaign potential.

Full Take

By analyzing content through the A.R.C., readers are encouraged to develop intellectual honesty and critical thinking skills
The system provides a structured approach for academic and scholarly content analysis, considering factors such as methodology, claims versus evidence, literature context, real-world implications, and bridge questions
For news/media analysis, it detects manipulation patterns, identifies root causes, examines implications, poses bridge questions, and assesses counter-influence campaign potential
The A.R.C.'s focus on promoting cognitive sovereignty is a response to the growing need for individuals to critically evaluate information in an increasingly complex and manipulative media landscape