kitty/glfw/ibus_glfw.c
Kovid Goyal a7cbe3776d
Wayland GNOME: Fix for ibus not working when using XWayland
See 8ce25208c3

I dont know what it is with GNOME. Every single release they break
backward compatibility somewhere, somehow. They must have special
talents.

Fixes #5967
2023-02-02 10:25:33 +05:30

524 lines
18 KiB
C
Vendored
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//========================================================================
// GLFW 3.4 XKB - www.glfw.org
//------------------------------------------------------------------------
// Copyright (c) 2018 Kovid Goyal <kovid@kovidgoyal.net>
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
// claim that you wrote the original software. If you use this software
// in a product, an acknowledgment in the product documentation would
// be appreciated but is not required.
//
// 2. Altered source versions must be plainly marked as such, and must not
// be misrepresented as being the original software.
//
// 3. This notice may not be removed or altered from any source
// distribution.
//
//========================================================================
/* To test under X11 start IBUS as:
* ibus-daemon -drxR
* Setup the input sources you want with:
* ibus-setup
* Switch to the input source you want to test with:
* ibus engine name
* You can list available engines with:
* ibus list-engine
* Then run kitty as:
* GLFW_IM_MODULE=ibus kitty
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <limits.h>
#include "internal.h"
#include "ibus_glfw.h"
#define debug(...) if (_glfw.hints.init.debugKeyboard) printf(__VA_ARGS__);
static const char IBUS_SERVICE[] = "org.freedesktop.IBus";
static const char IBUS_PATH[] = "/org/freedesktop/IBus";
static const char IBUS_INTERFACE[] = "org.freedesktop.IBus";
static const char IBUS_INPUT_INTERFACE[] = "org.freedesktop.IBus.InputContext";
enum Capabilities {
IBUS_CAP_PREEDIT_TEXT = 1 << 0,
IBUS_CAP_AUXILIARY_TEXT = 1 << 1,
IBUS_CAP_LOOKUP_TABLE = 1 << 2,
IBUS_CAP_FOCUS = 1 << 3,
IBUS_CAP_PROPERTY = 1 << 4,
IBUS_CAP_SURROUNDING_TEXT = 1 << 5
};
typedef enum
{
IBUS_SHIFT_MASK = 1 << 0,
IBUS_LOCK_MASK = 1 << 1,
IBUS_CONTROL_MASK = 1 << 2,
IBUS_MOD1_MASK = 1 << 3,
IBUS_MOD2_MASK = 1 << 4,
IBUS_MOD3_MASK = 1 << 5,
IBUS_MOD4_MASK = 1 << 6,
IBUS_MOD5_MASK = 1 << 7,
IBUS_BUTTON1_MASK = 1 << 8,
IBUS_BUTTON2_MASK = 1 << 9,
IBUS_BUTTON3_MASK = 1 << 10,
IBUS_BUTTON4_MASK = 1 << 11,
IBUS_BUTTON5_MASK = 1 << 12,
/* The next few modifiers are used by XKB, so we skip to the end.
* Bits 15 - 23 are currently unused. Bit 29 is used internally.
*/
/* ibus mask */
IBUS_HANDLED_MASK = 1 << 24,
IBUS_FORWARD_MASK = 1 << 25,
IBUS_IGNORED_MASK = IBUS_FORWARD_MASK,
IBUS_SUPER_MASK = 1 << 26,
IBUS_HYPER_MASK = 1 << 27,
IBUS_META_MASK = 1 << 28,
IBUS_RELEASE_MASK = 1 << 30,
IBUS_MODIFIER_MASK = 0x5f001fff
} IBusModifierType;
static uint32_t
ibus_key_state_from_glfw(unsigned int glfw_modifiers, int action) {
uint32_t ans = action == GLFW_RELEASE ? IBUS_RELEASE_MASK : 0;
#define M(g, i) if(glfw_modifiers & GLFW_MOD_##g) ans |= i
M(SHIFT, IBUS_SHIFT_MASK);
M(CAPS_LOCK, IBUS_LOCK_MASK);
M(CONTROL, IBUS_CONTROL_MASK);
M(ALT, IBUS_MOD1_MASK);
M(NUM_LOCK, IBUS_MOD2_MASK);
M(SUPER, IBUS_MOD4_MASK);
/* To do: figure out how to get super/hyper/meta */
#undef M
return ans;
}
static unsigned int
glfw_modifiers_from_ibus_state(uint32_t ibus_key_state) {
unsigned int ans = 0;
#define M(g, i) if(ibus_key_state & i) ans |= GLFW_MOD_##g
M(SHIFT, IBUS_SHIFT_MASK);
M(CAPS_LOCK, IBUS_LOCK_MASK);
M(CONTROL, IBUS_CONTROL_MASK);
M(ALT, IBUS_MOD1_MASK);
M(NUM_LOCK, IBUS_MOD2_MASK);
M(SUPER, IBUS_MOD4_MASK);
/* To do: figure out how to get super/hyper/meta */
#undef M
return ans;
}
static bool
test_env_var(const char *name, const char *val) {
const char *q = getenv(name);
return (q && strcmp(q, val) == 0) ? true : false;
}
static size_t
GLFW_MIN(size_t a, size_t b) {
return a < b ? a : b;
}
static const char*
get_ibus_text_from_message(DBusMessage *msg) {
/* The message structure is (from dbus-monitor)
variant struct {
string "IBusText"
array [
]
string "ash "
variant struct {
string "IBusAttrList"
array [
]
array [
]
}
}
*/
const char *text = NULL;
const char *struct_id = NULL;
DBusMessageIter iter, sub1, sub2;
dbus_message_iter_init(msg, &iter);
if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_VARIANT) return NULL;
dbus_message_iter_recurse(&iter, &sub1);
if (dbus_message_iter_get_arg_type(&sub1) != DBUS_TYPE_STRUCT) return NULL;
dbus_message_iter_recurse(&sub1, &sub2);
if (dbus_message_iter_get_arg_type(&sub2) != DBUS_TYPE_STRING) return NULL;
dbus_message_iter_get_basic(&sub2, &struct_id);
if (!struct_id || strncmp(struct_id, "IBusText", sizeof("IBusText")) != 0) return NULL;
dbus_message_iter_next(&sub2);
dbus_message_iter_next(&sub2);
if (dbus_message_iter_get_arg_type(&sub2) != DBUS_TYPE_STRING) return NULL;
dbus_message_iter_get_basic(&sub2, &text);
return text;
}
static void
handle_ibus_forward_key_event(DBusMessage *msg) {
uint32_t keysym, keycode, state;
DBusMessageIter iter;
dbus_message_iter_init(msg, &iter);
if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_UINT32) return;
dbus_message_iter_get_basic(&iter, &keysym);
dbus_message_iter_next(&iter);
if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_UINT32) return;
dbus_message_iter_get_basic(&iter, &keycode);
dbus_message_iter_next(&iter);
if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_UINT32) return;
dbus_message_iter_get_basic(&iter, &state);
int mods = glfw_modifiers_from_ibus_state(state);
debug("IBUS: ForwardKeyEvent: keysym=%x, keycode=%x, state=%x, glfw_mods=%x\n", keysym, keycode, state, mods);
glfw_xkb_forwarded_key_from_ime(keysym, mods);
}
static void
send_text(const char *text, GLFWIMEState ime_state) {
_GLFWwindow *w = _glfwFocusedWindow();
if (w && w->callbacks.keyboard) {
GLFWkeyevent fake_ev = {.action = GLFW_PRESS};
fake_ev.text = text;
fake_ev.ime_state = ime_state;
w->callbacks.keyboard((GLFWwindow*) w, &fake_ev);
}
}
// Connection handling {{{
static DBusHandlerResult
message_handler(DBusConnection *conn UNUSED, DBusMessage *msg, void *user_data) {
// To monitor signals from IBUS, use
//  dbus-monitor --address `ibus address` "type='signal',interface='org.freedesktop.IBus.InputContext'"
_GLFWIBUSData *ibus = (_GLFWIBUSData*)user_data;
(void)ibus;
const char *text;
switch(glfw_dbus_match_signal(msg, IBUS_INPUT_INTERFACE, "CommitText", "UpdatePreeditText", "HidePreeditText", "ShowPreeditText", "ForwardKeyEvent", NULL)) {
case 0:
text = get_ibus_text_from_message(msg);
debug("IBUS: CommitText: '%s'\n", text ? text : "(nil)");
send_text(text, GLFW_IME_COMMIT_TEXT);
break;
case 1:
text = get_ibus_text_from_message(msg);
debug("IBUS: UpdatePreeditText: '%s'\n", text ? text : "(nil)");
send_text(text, GLFW_IME_PREEDIT_CHANGED);
break;
case 2:
debug("IBUS: HidePreeditText\n");
send_text("", GLFW_IME_PREEDIT_CHANGED);
break;
case 3:
debug("IBUS: ShowPreeditText\n");
break;
case 4:
handle_ibus_forward_key_event(msg);
break;
}
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}
static DBusHandlerResult
ibus_on_owner_change(DBusConnection* conn UNUSED, DBusMessage* msg, void* user_data) {
if (dbus_message_is_signal(msg, "org.freedesktop.DBus", "NameOwnerChanged")) {
const char* name;
const char* old_owner;
const char* new_owner;
if (!dbus_message_get_args(msg, NULL,
DBUS_TYPE_STRING, &name,
DBUS_TYPE_STRING, &old_owner,
DBUS_TYPE_STRING, &new_owner,
DBUS_TYPE_INVALID
)) {
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}
if (strcmp(name, "org.freedesktop.IBus") != 0) {
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}
_GLFWIBUSData* ibus = (_GLFWIBUSData*) user_data;
ibus->name_owner_changed = true;
return DBUS_HANDLER_RESULT_HANDLED;
}
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}
static const char*
get_ibus_address_file_name(void) {
const char *addr;
static char ans[PATH_MAX];
static char display[64] = {0};
addr = getenv("IBUS_ADDRESS");
int offset = 0;
if (addr && addr[0]) {
memcpy(ans, addr, GLFW_MIN(strlen(addr), sizeof(ans)));
return ans;
}
const char* disp_num = NULL;
const char *host = "unix";
// See https://github.com/ibus/ibus/commit/8ce25208c3f4adfd290a032c6aa739d2b7580eb1 for why we need this dance.
const char *de = getenv("WAYLAND_DISPLAY");
if (de) {
disp_num = de;
} else {
const char *de = getenv("DISPLAY");
if (!de || !de[0]) de = ":0.0";
strncpy(display, de, sizeof(display) - 1);
char *dnum = strrchr(display, ':');
if (!dnum) {
_glfwInputError(GLFW_PLATFORM_ERROR, "Could not get IBUS address file name as DISPLAY env var has no colon");
return NULL;
}
char *screen_num = strrchr(display, '.');
*dnum = 0;
dnum++;
if (screen_num) *screen_num = 0;
if (*display) host = display;
disp_num = dnum;
}
memset(ans, 0, sizeof(ans));
const char *conf_env = getenv("XDG_CONFIG_HOME");
if (conf_env && conf_env[0]) {
offset = snprintf(ans, sizeof(ans), "%s", conf_env);
} else {
conf_env = getenv("HOME");
if (!conf_env || !conf_env[0]) {
_glfwInputError(GLFW_PLATFORM_ERROR, "Could not get IBUS address file name as no HOME env var is set");
return NULL;
}
offset = snprintf(ans, sizeof(ans), "%s/.config", conf_env);
}
char *key = dbus_get_local_machine_id();
snprintf(ans + offset, sizeof(ans) - offset, "/ibus/bus/%s-%s-%s", key, host, disp_num);
dbus_free(key);
return ans;
}
static bool
read_ibus_address(_GLFWIBUSData *ibus) {
static char buf[1024];
struct stat s;
FILE *addr_file = fopen(ibus->address_file_name, "r");
if (!addr_file) {
_glfwInputError(GLFW_PLATFORM_ERROR, "Failed to open IBUS address file: %s with error: %s", ibus->address_file_name, strerror(errno));
return false;
}
int stat_result = fstat(fileno(addr_file), &s);
bool found = false;
while (fgets(buf, sizeof(buf), addr_file)) {
if (strncmp(buf, "IBUS_ADDRESS=", sizeof("IBUS_ADDRESS=")-1) == 0) {
size_t sz = strlen(buf);
if (buf[sz-1] == '\n') buf[sz-1] = 0;
if (buf[sz-2] == '\r') buf[sz-2] = 0;
found = true;
break;
}
}
fclose(addr_file); addr_file = NULL;
if (stat_result != 0) {
_glfwInputError(GLFW_PLATFORM_ERROR, "Failed to stat IBUS address file: %s with error: %s", ibus->address_file_name, strerror(errno));
return false;
}
ibus->address_file_mtime = s.st_mtime;
if (found) {
free((void*)ibus->address);
ibus->address = _glfw_strdup(buf + sizeof("IBUS_ADDRESS=") - 1);
return true;
}
_glfwInputError(GLFW_PLATFORM_ERROR, "Could not find IBUS_ADDRESS in %s", ibus->address_file_name);
return false;
}
void
input_context_created(DBusMessage *msg, const char* errmsg, void *data) {
if (errmsg) {
_glfwInputError(GLFW_PLATFORM_ERROR, "IBUS: Failed to create input context with error: %s", errmsg);
return;
}
const char *path = NULL;
if (!glfw_dbus_get_args(msg, "Failed to get IBUS context path from reply", DBUS_TYPE_OBJECT_PATH, &path, DBUS_TYPE_INVALID)) return;
_GLFWIBUSData *ibus = (_GLFWIBUSData*)data;
free((void*)ibus->input_ctx_path);
ibus->input_ctx_path = _glfw_strdup(path);
if (!ibus->input_ctx_path) return;
dbus_bus_add_match(ibus->conn, "type='signal',interface='org.freedesktop.DBus', member='NameOwnerChanged'", NULL);
dbus_connection_add_filter(ibus->conn, ibus_on_owner_change, ibus, free);
dbus_bus_add_match(ibus->conn, "type='signal',interface='org.freedesktop.IBus.InputContext'", NULL);
DBusObjectPathVTable ibus_vtable = {.message_function = message_handler};
dbus_connection_try_register_object_path(ibus->conn, ibus->input_ctx_path, &ibus_vtable, ibus, NULL);
enum Capabilities caps = IBUS_CAP_FOCUS | IBUS_CAP_PREEDIT_TEXT;
if (!glfw_dbus_call_method_no_reply(ibus->conn, IBUS_SERVICE, ibus->input_ctx_path, IBUS_INPUT_INTERFACE, "SetCapabilities", DBUS_TYPE_UINT32, &caps, DBUS_TYPE_INVALID)) return;
ibus->ok = true;
glfw_ibus_set_focused(ibus, _glfwFocusedWindow() != NULL);
glfw_ibus_set_cursor_geometry(ibus, 0, 0, 0, 0);
debug("Connected to IBUS daemon for IME input management\n");
}
static bool
setup_connection(_GLFWIBUSData *ibus) {
const char *client_name = "GLFW_Application";
const char *address_file_name = get_ibus_address_file_name();
ibus->ok = false;
if (!address_file_name) return false;
free((void*)ibus->address_file_name);
ibus->address_file_name = _glfw_strdup(address_file_name);
if (!read_ibus_address(ibus)) return false;
if (ibus->conn) {
glfw_dbus_close_connection(ibus->conn);
ibus->conn = NULL;
}
debug("Connecting to IBUS daemon @ %s for IME input management\n", ibus->address);
ibus->conn = glfw_dbus_connect_to(ibus->address, "Failed to connect to the IBUS daemon, with error", "ibus", true);
if (!ibus->conn) return false;
free((void*)ibus->input_ctx_path); ibus->input_ctx_path = NULL;
if (!glfw_dbus_call_method_with_reply(
ibus->conn, IBUS_SERVICE, IBUS_PATH, IBUS_INTERFACE, "CreateInputContext", DBUS_TIMEOUT_USE_DEFAULT, input_context_created, ibus,
DBUS_TYPE_STRING, &client_name, DBUS_TYPE_INVALID)) {
return false;
}
return true;
}
void
glfw_connect_to_ibus(_GLFWIBUSData *ibus) {
if (ibus->inited) return;
if (!test_env_var("GLFW_IM_MODULE", "ibus")) return;
ibus->inited = true;
ibus->name_owner_changed = false;
setup_connection(ibus);
}
void
glfw_ibus_terminate(_GLFWIBUSData *ibus) {
if (ibus->conn) {
glfw_dbus_close_connection(ibus->conn);
ibus->conn = NULL;
}
#define F(x) if (ibus->x) { free((void*)ibus->x); ibus->x = NULL; }
F(input_ctx_path);
F(address);
F(address_file_name);
#undef F
ibus->ok = false;
}
static bool
check_connection(_GLFWIBUSData *ibus) {
if (!ibus->inited) return false;
if (ibus->conn && dbus_connection_get_is_connected(ibus->conn) && !ibus->name_owner_changed) return ibus->ok;
struct stat s;
ibus->name_owner_changed = false;
if (stat(ibus->address_file_name, &s) != 0 || s.st_mtime != ibus->address_file_mtime) {
if (!read_ibus_address(ibus)) return false;
return setup_connection(ibus);
}
return false;
}
void
glfw_ibus_dispatch(_GLFWIBUSData *ibus) {
if (ibus->conn) glfw_dbus_dispatch(ibus->conn);
}
// }}}
static void
simple_message(_GLFWIBUSData *ibus, const char *method) {
if (check_connection(ibus)) {
glfw_dbus_call_method_no_reply(ibus->conn, IBUS_SERVICE, ibus->input_ctx_path, IBUS_INPUT_INTERFACE, method, DBUS_TYPE_INVALID);
}
}
void
glfw_ibus_set_focused(_GLFWIBUSData *ibus, bool focused) {
simple_message(ibus, focused ? "FocusIn" : "FocusOut");
}
void
glfw_ibus_set_cursor_geometry(_GLFWIBUSData *ibus, int x, int y, int w, int h) {
if (check_connection(ibus)) {
glfw_dbus_call_method_no_reply(ibus->conn, IBUS_SERVICE, ibus->input_ctx_path, IBUS_INPUT_INTERFACE, "SetCursorLocation",
DBUS_TYPE_INT32, &x, DBUS_TYPE_INT32, &y, DBUS_TYPE_INT32, &w, DBUS_TYPE_INT32, &h, DBUS_TYPE_INVALID);
}
}
void
key_event_processed(DBusMessage *msg, const char* errmsg, void *data) {
uint32_t handled = 0;
_GLFWIBUSKeyEvent *ev = (_GLFWIBUSKeyEvent*)data;
// Restore key's text from the text embedded in the structure.
ev->glfw_ev.text = ev->__embedded_text;
bool is_release = ev->glfw_ev.action == GLFW_RELEASE;
bool failed = false;
if (errmsg) {
_glfwInputError(GLFW_PLATFORM_ERROR, "IBUS: Failed to process key with error: %s", errmsg);
failed = true;
} else {
glfw_dbus_get_args(msg, "Failed to get IBUS handled key from reply", DBUS_TYPE_BOOLEAN, &handled, DBUS_TYPE_INVALID);
debug("IBUS processed native_key: 0x%x release: %d handled: %u\n", ev->glfw_ev.native_key, is_release, handled);
}
glfw_xkb_key_from_ime(ev, handled ? true : false, failed);
free(ev);
}
bool
ibus_process_key(const _GLFWIBUSKeyEvent *ev_, _GLFWIBUSData *ibus) {
if (!check_connection(ibus)) return false;
_GLFWIBUSKeyEvent *ev = calloc(1, sizeof(_GLFWIBUSKeyEvent));
if (!ev) return false;
memcpy(ev, ev_, sizeof(_GLFWIBUSKeyEvent));
// Put the key's text in a field IN the structure, for proper serialization.
if (ev->glfw_ev.text) strncpy(ev->__embedded_text, ev->glfw_ev.text, sizeof(ev->__embedded_text) - 1);
ev->glfw_ev.text = NULL;
uint32_t state = ibus_key_state_from_glfw(ev->glfw_ev.mods, ev->glfw_ev.action);
if (!glfw_dbus_call_method_with_reply(
ibus->conn, IBUS_SERVICE, ibus->input_ctx_path, IBUS_INPUT_INTERFACE, "ProcessKeyEvent",
3000, key_event_processed, ev,
DBUS_TYPE_UINT32, &ev->ibus_keysym, DBUS_TYPE_UINT32, &ev->ibus_keycode, DBUS_TYPE_UINT32,
&state, DBUS_TYPE_INVALID)) {
free(ev);
return false;
}
return true;
}