내 i3‑Emacs 연동
출처: Hacker News
타일링 윈도우 매니저는 정말 멋집니다. 초유연 텍스트 편집기도 역시 멋지죠. 한 번은 EXWM이 이상적인 해결책이라고 생각했는데… 텍스트 버퍼보다 일반적인 그래픽 윈도우를 더 많이, 혹은 똑같이 사용하고, 때때로 그 윈도우가 EXWM의 화려한 입력 방식과 충돌하는 프로그램(예: Steam)이라서요.
하지만 저는 여전히 Emacs를 아주 좋아합니다. 심지어 제 머신에서는 밝은 모드와 어두운 모드를 전환해 주기도 하죠(아직도!). 그래서 (\sqrt{-1})‘s 같은 글에 영감을 받아 Emacs와 i3 사이에 공통된 키 바인딩을 만들고, 터미널 열기, 창 분할 등에 대한 합리적인 기본값을 정해 보기로 했습니다.
우선 위에서 언급한 글처럼 xdotool과 emacsclient를 이용한 스크립트를 시도했는데, 동작은 했지만 너무 느렸습니다. 최대 1초 정도 지연이 있었고, 스크립트 자체는 호출부터 종료까지 30~100 ms 정도 걸렸습니다. 아직도 그 지연이 어디서 오는지 정확히 알지 못합니다. Emacs에 입력을 보내고 실제로 인식되기까지의 시간 때문인지, Emacs 버전이나 다른 패키지, emacsclient의 특이점 때문인지 모르겠지만, 이대로는 부족했습니다. 게다가 가장 자주 사용하는 키 조합을 위해 전체 쉘을 띄우는 것은 낭비라고 생각했습니다. 그래서 합리적인 선택을 했습니다: i3를 직접 패치했습니다.
목표는 간단했습니다. i3의 bindsym으로 바인딩된 명령을 일방적으로 처리하는 대신, 현재 포커스된 창이 Emacs인지 확인하고, 맞다면 키 이벤트를 Emacs에 전달하도록 옵션을 추가하는 것이었습니다. 이 기능은 과거에 요청된 적도 있지만, i3 유지관리자는 범위 밖이라고 판단했습니다. 상황이 달라진다면 더 완전한 패치를 만들 수도 있겠죠. Emacs가 “아니, 이건 i3가 처리해야 한다”고 판단하면 i3-msg를 사용해 다시 i3로 라우팅할 수 있습니다.
결국 성공했지만, 가장 우아한 방법은 아닐 수도 있습니다. xcb에 대해 조언해 주실 분은 언제든지 환영합니다! 아래 이메일로 연락 주세요: web@khz.ac.
목차
관련 i3 코드
i3는 xcb_grab_key()를 owner_events = 0으로 루트 X 윈도우에 적용해 키를 가로챕니다. src/bindings.c에 있는 관련 코드는 다음과 같습니다. 모든 코드 조각은 i3 4.25.1 기준이며, 따라가시려면 참고하세요.
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}
이 코드는 크게 중요하지는 않지만, i3가 루트 윈도우에서 모든 바인딩을 가로채는 방식을 보여줍니다. owner_events = 1로 설정해 이벤트를 통과시키면 루트 윈도우에만 전달된다고 하니, 우리가 원하는 동작은 아닙니다.
i3의 handle_event() (src/handlers.c)에서는 xcb 이벤트를 받으면 타입에 따라 특화된 핸들러로 넘깁니다:
1481switch (type) {
1482case XCB_KEY_PRESS:
1483case XCB_KEY_RELEASE:
1484 handle_key_press((xcb_key_press_event_t *)event);
1485 break;
1486 }
handle_key_press() (src/key_press.c)는 키 이벤트를 받아 바인딩을 찾아 해당 명령을 실행합니다:
12
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
28 if (bind == NULL) {
29 return;
30 }
31
32 CommandResult *result = run_binding(bind, NULL);
33 command_result_free(result);
34}
특히 이 함수는 원본 xcb_key_press_event_t를 받으며, 이를 xcb_send_event()로 바로 재전송할 수 있다는 것을 깨달았습니다. 다만 이벤트를 받는 창이 포커스를 잃게 되는 문제가 남아 있습니다. 해결 방법을 아시는 분은 알려 주세요.
여기가 변경을 가하기에 적절한 위치처럼 보입니다!
패치
Binding 구조체 변경
include/data.h에 Binding에 새로운 필드를 추가해, 해당 바인딩이 직접 이벤트를 받아야 할 창 클래스를 지정하도록 했습니다:
/**
* 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;
};
바인딩 초기화 시 passthrough 값을 설정하도록 수정했습니다(정리 코드는 생략). 패치 파일을 보시면 전체 내용을 확인할 수 있습니다.
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() 수정
이제 handle_key_press()는 bind->passthrough.class가 설정돼 있는지 확인하고, 현재 포커스된 창의 클래스가 일치하면 키 이벤트를 해당 창에 재전송합니다. 인터셉션을 비활성화해야 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;