From 7619d2bd9d62bb9af69a5ce0ac7b69bce28cecea Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 14 Aug 2026 19:59:52 +0900 Subject: [PATCH] ibus: support modern client preedit --- .gitignore | 1 + Dockerfile | 1 + ibus.c | 164 ++++++++++++++++++----- tests/Makefile | 12 +- tests/ibus_client_smoke.c | 154 +++++++++++++++++++++ tests/ibus_live_test.c | 272 ++++++++++++++++++++++++++++++++++++-- 6 files changed, 557 insertions(+), 47 deletions(-) create mode 100644 tests/ibus_client_smoke.c diff --git a/.gitignore b/.gitignore index 86f6050..967fe90 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ /tests/*.o /tests/unit_test /tests/ibus_live_test +/tests/ibus_client_smoke /tests/gtk_live_test /tests/ipc_live_test /tests/daemon_collision_test diff --git a/Dockerfile b/Dockerfile index a4dc2a3..42b9e66 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,7 @@ RUN pacman -Syu --noconfirm --needed \ diffutils \ gcc \ gtk3 \ + ibus \ harfbuzz \ libxcb \ libxkbcommon \ diff --git a/ibus.c b/ibus.c index 99aac59..e111f6f 100644 --- a/ibus.c +++ b/ibus.c @@ -22,6 +22,9 @@ enum Ibuspurposepassword = 8, Ibuspurposepin = 9, Ibushinthidden = 1<<12, + Ibusattrunderline = 1, + Ibusunderlinesingle = 1, + Ibuspreeditclear = 0, }; typedef struct Ictx Ictx; @@ -33,6 +36,7 @@ struct Ictx u32int cap; u32int purpose; u32int hints; + int clientcommitpreedit; Caret caret; }; @@ -49,8 +53,11 @@ static int addrowned; static int icctr; static int busctr; static Channel *replyc; +static const char ibusowner[] = ":1.0"; static DBusHandlerResult onmsg(DBusConnection*, DBusMessage*, void*); +static DBusHandlerResult handleerror(DBusConnection*, DBusMessage*, + const char*, const char*); static void unlinkaddr(void) @@ -380,11 +387,14 @@ setcursor(Ictx *ctx, int x, int y, int h) } static void -appendibustext(DBusMessageIter *it, const char *s) +appendibustext(DBusMessageIter *it, const char *s, int underline) { DBusMessageIter v, st, attach, alv, ali, attr, alist; + DBusMessageIter av, ai, aattach; const char *name = "IBusText"; const char *aname = "IBusAttrList"; + const char *iname = "IBusAttribute"; + dbus_uint32_t type, value, start, end; dbus_message_iter_open_container(it, DBUS_TYPE_VARIANT, "(sa{sv}sv)", &v); dbus_message_iter_open_container(&v, DBUS_TYPE_STRUCT, nil, &st); @@ -398,6 +408,25 @@ appendibustext(DBusMessageIter *it, const char *s) dbus_message_iter_open_container(&ali, DBUS_TYPE_ARRAY, "{sv}", &attr); dbus_message_iter_close_container(&ali, &attr); dbus_message_iter_open_container(&ali, DBUS_TYPE_ARRAY, "v", &alist); + if(underline && s[0] != '\0'){ + type = Ibusattrunderline; + value = Ibusunderlinesingle; + start = 0; + end = utflen((char*)s); + dbus_message_iter_open_container(&alist, DBUS_TYPE_VARIANT, + "(sa{sv}uuuu)", &av); + dbus_message_iter_open_container(&av, DBUS_TYPE_STRUCT, nil, &ai); + dbus_message_iter_append_basic(&ai, DBUS_TYPE_STRING, &iname); + dbus_message_iter_open_container(&ai, DBUS_TYPE_ARRAY, "{sv}", + &aattach); + dbus_message_iter_close_container(&ai, &aattach); + dbus_message_iter_append_basic(&ai, DBUS_TYPE_UINT32, &type); + dbus_message_iter_append_basic(&ai, DBUS_TYPE_UINT32, &value); + dbus_message_iter_append_basic(&ai, DBUS_TYPE_UINT32, &start); + dbus_message_iter_append_basic(&ai, DBUS_TYPE_UINT32, &end); + dbus_message_iter_close_container(&av, &ai); + dbus_message_iter_close_container(&alist, &av); + } dbus_message_iter_close_container(&ali, &alist); dbus_message_iter_close_container(&alv, &ali); dbus_message_iter_close_container(&st, &alv); @@ -415,30 +444,39 @@ emitcommit(DBusConnection *c, const char *path, const char *text) if(sig == nil) return; dbus_message_iter_init_append(sig, &it); - appendibustext(&it, text); + appendibustext(&it, text, 0); + dbus_message_set_sender(sig, ibusowner); dbus_connection_send(c, sig, nil); dbus_message_unref(sig); } static void -emitpreedit(DBusConnection *c, const char *path, const char *text) +emitpreedit(Ictx *ctx, const char *text) { DBusMessage *sig; DBusMessageIter it; - dbus_uint32_t cursor; + dbus_uint32_t cursor, mode; dbus_bool_t visible; + const char *name; - sig = dbus_message_new_signal(path, - "org.freedesktop.IBus.InputContext", "UpdatePreeditText"); + name = ctx->clientcommitpreedit ? + "UpdatePreeditTextWithMode" : "UpdatePreeditText"; + sig = dbus_message_new_signal(ctx->path, + "org.freedesktop.IBus.InputContext", name); if(sig == nil) return; dbus_message_iter_init_append(sig, &it); - appendibustext(&it, text); + appendibustext(&it, text, 1); cursor = utflen((char*)text); visible = text[0] != '\0' ? TRUE : FALSE; dbus_message_iter_append_basic(&it, DBUS_TYPE_UINT32, &cursor); dbus_message_iter_append_basic(&it, DBUS_TYPE_BOOLEAN, &visible); - dbus_connection_send(c, sig, nil); + if(ctx->clientcommitpreedit){ + mode = Ibuspreeditclear; + dbus_message_iter_append_basic(&it, DBUS_TYPE_UINT32, &mode); + } + dbus_message_set_sender(sig, ibusowner); + dbus_connection_send(ctx->conn, sig, nil); dbus_message_unref(sig); } @@ -459,6 +497,33 @@ handlehello(DBusConnection *c, DBusMessage *m) return DBUS_HANDLER_RESULT_HANDLED; } +static DBusHandlerResult +handlenameowner(DBusConnection *c, DBusMessage *m) +{ + DBusError err; + DBusMessage *r; + const char *name, *owner; + + dbus_error_init(&err); + if(!dbus_message_get_args(m, &err, DBUS_TYPE_STRING, &name, + DBUS_TYPE_INVALID)){ + dbus_error_free(&err); + return handleerror(c, m, DBUS_ERROR_INVALID_ARGS, + "GetNameOwner expects one bus name"); + } + if(strcmp(name, "org.freedesktop.IBus") != 0) + return handleerror(c, m, DBUS_ERROR_NAME_HAS_NO_OWNER, + "bus name has no owner"); + owner = ibusowner; + r = dbus_message_new_method_return(m); + if(r == nil) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + dbus_message_append_args(r, DBUS_TYPE_STRING, &owner, DBUS_TYPE_INVALID); + dbus_connection_send(c, r, nil); + dbus_message_unref(r); + return DBUS_HANDLER_RESULT_HANDLED; +} + static DBusHandlerResult handleerror(DBusConnection *c, DBusMessage *m, const char *name, const char *text) @@ -539,7 +604,7 @@ handlekey(DBusConnection *c, DBusMessage *m, Ictx *ctx) if(commit[0] != '\0') emitcommit(c, dbus_message_get_path(m), commit); if(ctx->cap & Ibuscappreedit) - emitpreedit(c, dbus_message_get_path(m), preedit); + emitpreedit(ctx, preedit); return handlebool(c, m, res.eaten); } @@ -560,7 +625,7 @@ handlereset(DBusConnection *c, DBusMessage *m, Ictx *ctx) Keyres res; sendrequest(ctx, Keyreset, 0, 0, &res); - emitpreedit(c, dbus_message_get_path(m), ""); + emitpreedit(ctx, ""); return handlenoop(c, m); } @@ -575,7 +640,7 @@ static DBusHandlerResult handlefocusout(DBusConnection *c, DBusMessage *m, Ictx *ctx) { releasecontext(ctx); - emitpreedit(c, dbus_message_get_path(m), ""); + emitpreedit(ctx, ""); return handlenoop(c, m); } @@ -602,20 +667,19 @@ handlecap(DBusConnection *c, DBusMessage *m, Ictx *ctx) if((old & Ibuscappreedit) != (ctx->cap & Ibuscappreedit)){ if((ctx->cap & Ibuscappreedit) && res.eaten){ stoutf(&res.preedit, preedit, sizeof preedit); - emitpreedit(c, ctx->path, preedit); + emitpreedit(ctx, preedit); }else if(!(ctx->cap & Ibuscappreedit)) - emitpreedit(c, ctx->path, ""); + emitpreedit(ctx, ""); } } return handlenoop(c, m); } static int -getcontent(DBusMessage *m, const char **iface, const char **name, - u32int *purpose, u32int *hints) +getproperty(DBusMessage *m, const char **iface, const char **name, + DBusMessageIter *value) { - DBusMessageIter it, v, st; - dbus_uint32_t p, h; + DBusMessageIter it; if(!dbus_message_has_signature(m, "ssv") || !dbus_message_iter_init(m, &it)) @@ -624,10 +688,19 @@ getcontent(DBusMessage *m, const char **iface, const char **name, dbus_message_iter_next(&it); dbus_message_iter_get_basic(&it, name); dbus_message_iter_next(&it); - dbus_message_iter_recurse(&it, &v); - if(dbus_message_iter_get_arg_type(&v) != DBUS_TYPE_STRUCT) + dbus_message_iter_recurse(&it, value); + return 1; +} + +static int +getcontent(DBusMessageIter *value, u32int *purpose, u32int *hints) +{ + DBusMessageIter st; + dbus_uint32_t p, h; + + if(dbus_message_iter_get_arg_type(value) != DBUS_TYPE_STRUCT) return 0; - dbus_message_iter_recurse(&v, &st); + dbus_message_iter_recurse(value, &st); if(dbus_message_iter_get_arg_type(&st) != DBUS_TYPE_UINT32) return 0; dbus_message_iter_get_basic(&st, &p); @@ -646,27 +719,48 @@ static DBusHandlerResult handlepropertyset(DBusConnection *c, DBusMessage *m, Ictx *ctx) { const char *iface, *name; + DBusMessageIter value, st; + dbus_bool_t b; u32int purpose, hints; Keyres res; int washidden; - if(!getcontent(m, &iface, &name, &purpose, &hints)) + if(!getproperty(m, &iface, &name, &value)) return handleerror(c, m, DBUS_ERROR_INVALID_ARGS, - "Properties.Set expects ContentType as (uu)"); + "Properties.Set expects interface, property, and value"); if(strcmp(iface, "org.freedesktop.IBus.InputContext") != 0) return handleerror(c, m, DBUS_ERROR_UNKNOWN_INTERFACE, "unknown property interface"); - if(strcmp(name, "ContentType") != 0) - return handleerror(c, m, DBUS_ERROR_UNKNOWN_PROPERTY, - "unknown input context property"); - washidden = hidden(ctx); - ctx->purpose = purpose; - ctx->hints = hints; - if(ctx->focused && !washidden && hidden(ctx)){ - sendrequest(ctx, Keyreset, 0, 0, &res); - emitpreedit(c, ctx->path, ""); + if(strcmp(name, "ContentType") == 0){ + if(!getcontent(&value, &purpose, &hints)) + return handleerror(c, m, DBUS_ERROR_INVALID_ARGS, + "ContentType expects (uu)"); + washidden = hidden(ctx); + ctx->purpose = purpose; + ctx->hints = hints; + if(ctx->focused && !washidden && hidden(ctx)){ + sendrequest(ctx, Keyreset, 0, 0, &res); + emitpreedit(ctx, ""); + } + return handlenoop(c, m); } - return handlenoop(c, m); + if(strcmp(name, "ClientCommitPreedit") == 0){ + if(dbus_message_iter_get_arg_type(&value) != DBUS_TYPE_STRUCT) + return handleerror(c, m, DBUS_ERROR_INVALID_ARGS, + "ClientCommitPreedit expects (b)"); + dbus_message_iter_recurse(&value, &st); + if(dbus_message_iter_get_arg_type(&st) != DBUS_TYPE_BOOLEAN) + return handleerror(c, m, DBUS_ERROR_INVALID_ARGS, + "ClientCommitPreedit expects (b)"); + dbus_message_iter_get_basic(&st, &b); + if(dbus_message_iter_next(&st)) + return handleerror(c, m, DBUS_ERROR_INVALID_ARGS, + "ClientCommitPreedit expects (b)"); + ctx->clientcommitpreedit = b != FALSE; + return handlenoop(c, m); + } + return handleerror(c, m, DBUS_ERROR_UNKNOWN_PROPERTY, + "unknown input context property"); } static DBusHandlerResult @@ -778,9 +872,13 @@ static const char introspectxml[] = "\n" " \n" " \n" +" \n" " \n" " " "\n" +" " +"" +"\n" " \n" " \n" " " @@ -825,6 +923,8 @@ onmsg(DBusConnection *c, DBusMessage *m, void *_) if(strcmp(iface, "org.freedesktop.DBus") == 0){ if(strcmp(member, "Hello") == 0) return handlehello(c, m); + if(strcmp(member, "GetNameOwner") == 0) + return handlenameowner(c, m); if(strcmp(member, "AddMatch") == 0 || strcmp(member, "RemoveMatch") == 0) return handlenoop(c, m); diff --git a/tests/Makefile b/tests/Makefile index 7fb0b74..a1be7ba 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -10,13 +10,15 @@ GTK_CFLAGS = $(shell pkg-config --cflags gtk+-3.0) GTK_LIBS = $(shell pkg-config --libs gtk+-3.0) IBUS_CFLAGS = $(shell pkg-config --cflags dbus-1 xkbcommon) IBUS_LIBS = $(shell pkg-config --libs dbus-1 xkbcommon) +IBUS_CLIENT_CFLAGS = $(shell pkg-config --cflags ibus-1.0) +IBUS_CLIENT_LIBS = $(shell pkg-config --libs ibus-1.0) XIM_CFLAGS = $(shell pkg-config --cflags xcb-imdkit xkbcommon) XIM_LIBS = $(shell pkg-config --libs xcb-imdkit xkbcommon) LIBS = -lthread -lbio $(TEXT_LIBS) $(IBUS_LIBS) $(XIM_LIBS) PROG = unit_test -LIVE = ibus_live_test gtk_live_test ipc_live_test daemon_collision_test daemon_failure_test \ - daemon_restart_test +LIVE = ibus_live_test ibus_client_smoke gtk_live_test ipc_live_test \ + daemon_collision_test daemon_failure_test daemon_restart_test TESTSRC = unit_test.c test_util.c str_test.c hash_test.c trie_test.c \ ko_test.c vi_test.c engine_test.c dict_test.c ipc_test.c \ popup_test.c font_test.c ibus_test.c server_test.c xim_adapter_test.c @@ -31,7 +33,7 @@ all: $(PROG) $(LIVE) check test: $(PROG) $(LIVE) ./$(PROG) $(TESTARGS) - ./ibus_live_test ../strans ../map + ./ibus_live_test ../strans ../map ./ibus_client_smoke ./gtk_live_test ../gtk/im-strans.so ./ipc_live_test ../strans ../map ./daemon_collision_test ../strans ../map @@ -44,6 +46,10 @@ $(PROG): $(OBJS) ibus_live_test: ibus_live_test.c $(HOSTCC) -std=c99 -Wall -Wextra -O2 -g $(DBUS_CFLAGS) -o $@ $< $(DBUS_LIBS) +ibus_client_smoke: ibus_client_smoke.c + $(HOSTCC) -std=c99 -Wall -Wextra -O2 -g $(IBUS_CLIENT_CFLAGS) -o $@ $< \ + $(IBUS_CLIENT_LIBS) + gtk_live_test: gtk_live_test.c ../ipc.c ../ipc.h $(HOSTCC) -std=c99 -Wall -Wextra -O2 -g -I.. $(GTK_CFLAGS) -o $@ \ gtk_live_test.c ../ipc.c $(GTK_LIBS) -pthread diff --git a/tests/ibus_client_smoke.c b/tests/ibus_client_smoke.c new file mode 100644 index 0000000..d1377fb --- /dev/null +++ b/tests/ibus_client_smoke.c @@ -0,0 +1,154 @@ +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include + +typedef struct Log Log; +struct Log +{ + GMainLoop *loop; + int want; + int legacy; + int modern; + int invalid; + int sawtext; + int sawempty; +}; + +static void +legacy(IBusInputContext *ctx, IBusText *text, guint cursor, + gboolean visible, void *arg) +{ + Log *log; + + (void)ctx; + (void)text; + (void)cursor; + (void)visible; + log = arg; + log->legacy++; +} + +static void +modern(IBusInputContext *ctx, IBusText *text, guint cursor, + gboolean visible, guint mode, void *arg) +{ + IBusAttrList *attrs; + IBusAttribute *a; + Log *log; + const char *s; + + (void)ctx; + log = arg; + log->modern++; + s = ibus_text_get_text(text); + attrs = ibus_text_get_attributes(text); + a = ibus_attr_list_get(attrs, 0); + if(s[0] != '\0'){ + if(strcmp(s, "k") != 0 || cursor != 1 || !visible || + mode != IBUS_ENGINE_PREEDIT_CLEAR || a == NULL || + a->type != IBUS_ATTR_TYPE_UNDERLINE || + a->value != IBUS_ATTR_UNDERLINE_SINGLE || + a->start_index != 0 || a->end_index != 1 || + ibus_attr_list_get(attrs, 1) != NULL) + log->invalid = 1; + else + log->sawtext = 1; + }else{ + if(cursor != 0 || visible || mode != IBUS_ENGINE_PREEDIT_CLEAR || + a != NULL) + log->invalid = 1; + else + log->sawempty = 1; + } + if(log->loop != NULL && log->modern >= log->want) + g_main_loop_quit(log->loop); +} + +static gboolean +timeout(void *arg) +{ + g_main_loop_quit(arg); + return G_SOURCE_REMOVE; +} + +static int +waitmodern(Log *log, int want) +{ + guint timer; + + log->want = want; + if(log->modern >= want) + return 1; + log->loop = g_main_loop_new(NULL, FALSE); + timer = g_timeout_add_seconds(4, timeout, log->loop); + g_main_loop_run(log->loop); + if(g_main_context_find_source_by_id(NULL, timer) != NULL) + g_source_remove(timer); + g_main_loop_unref(log->loop); + log->loop = NULL; + return log->modern >= want; +} + +int +main(int argc, char **argv) +{ + IBusBus *bus; + IBusInputContext *ctx; + Log log; + int ok; + + if(argc != 2){ + fprintf(stderr, "usage: ibus_client_smoke address\n"); + return 2; + } + if(setenv("IBUS_ADDRESS", argv[1], 1) < 0 || + unsetenv("DBUS_SESSION_BUS_ADDRESS") < 0 || unsetenv("DISPLAY") < 0){ + perror("ibus_client_smoke: environment"); + return 1; + } + memset(&log, 0, sizeof log); + ibus_init(); + bus = ibus_bus_new(); + if(bus == NULL || !ibus_bus_is_connected(bus)){ + fprintf(stderr, "ibus_client_smoke: cannot connect to private bus\n"); + if(bus != NULL) g_object_unref(bus); + return 1; + } + ctx = ibus_bus_create_input_context(bus, "strans-libibus-smoke"); + if(ctx == NULL){ + fprintf(stderr, "ibus_client_smoke: cannot create input context\n"); + g_object_unref(bus); + return 1; + } + g_signal_connect(ctx, "update-preedit-text", G_CALLBACK(legacy), &log); + g_signal_connect(ctx, "update-preedit-text-with-mode", G_CALLBACK(modern), + &log); + ibus_input_context_set_capabilities(ctx, IBUS_CAP_PREEDIT_TEXT); + ibus_input_context_set_client_commit_preedit(ctx, TRUE); + ibus_input_context_focus_in(ctx); + ok = ibus_input_context_process_key_event(ctx, 'n', 0, + IBUS_CONTROL_MASK) && waitmodern(&log, 1) && + ibus_input_context_process_key_event(ctx, 'k', 0, 0) && + waitmodern(&log, 2); + ibus_input_context_reset(ctx); + if(ok) + ok = waitmodern(&log, 3); + ibus_input_context_focus_out(ctx); + if(ok) + ok = waitmodern(&log, 4); + if(log.legacy != 0 || log.modern != 4 || log.invalid || + !log.sawtext || !log.sawempty) + ok = 0; + g_object_unref(ctx); + g_object_unref(bus); + if(!ok){ + fprintf(stderr, + "ibus_client_smoke: legacy=%d modern=%d invalid=%d text=%d empty=%d\n", + log.legacy, log.modern, log.invalid, log.sawtext, log.sawempty); + return 1; + } + printf("official libibus client preedit: ok\n"); + return 0; +} diff --git a/tests/ibus_live_test.c b/tests/ibus_live_test.c index de8503c..895e6e4 100644 --- a/tests/ibus_live_test.c +++ b/tests/ibus_live_test.c @@ -35,6 +35,7 @@ enum }; typedef struct Daemon Daemon; +typedef struct Attrlog Attrlog; typedef struct Siglog Siglog; struct Daemon @@ -54,17 +55,31 @@ struct Daemon size_t nerr; }; +struct Attrlog +{ + int n; + dbus_uint32_t type; + dbus_uint32_t value; + dbus_uint32_t start; + dbus_uint32_t end; +}; + struct Siglog { int invalid; int npreedit; + int nlegacy; + int nmodern; int ncommit; char preeditpath[96]; char commitpath[96]; char preedit[256]; char commit[256]; dbus_uint32_t cursor; + dbus_uint32_t mode; dbus_bool_t visible; + Attrlog preeditattrs; + Attrlog commitattrs; }; static int @@ -497,10 +512,44 @@ emptyarray(DBusMessageIter *it, int element) } static int -ibusattrs(DBusMessageIter *it) +ibusattribute(DBusMessageIter *it, Attrlog *log) { DBusMessageIter v, st; const char *name; + dbus_uint32_t *field[] = { + &log->type, &log->value, &log->start, &log->end, + }; + int i; + + if(log->n != 0 || dbus_message_iter_get_arg_type(it) != DBUS_TYPE_VARIANT) + return 0; + dbus_message_iter_recurse(it, &v); + if(dbus_message_iter_get_arg_type(&v) != DBUS_TYPE_STRUCT) + return 0; + dbus_message_iter_recurse(&v, &st); + if(dbus_message_iter_get_arg_type(&st) != DBUS_TYPE_STRING) + return 0; + dbus_message_iter_get_basic(&st, &name); + if(strcmp(name, "IBusAttribute") != 0 || !dbus_message_iter_next(&st) || + !emptyarray(&st, DBUS_TYPE_DICT_ENTRY)) + return 0; + for(i = 0; i < 4; i++){ + if(!dbus_message_iter_next(&st) || + dbus_message_iter_get_arg_type(&st) != DBUS_TYPE_UINT32) + return 0; + dbus_message_iter_get_basic(&st, field[i]); + } + if(dbus_message_iter_next(&st) || dbus_message_iter_next(&v)) + return 0; + log->n++; + return 1; +} + +static int +ibusattrs(DBusMessageIter *it, Attrlog *log) +{ + DBusMessageIter v, st, a; + const char *name; if(dbus_message_iter_get_arg_type(it) != DBUS_TYPE_VARIANT) return 0; @@ -512,15 +561,23 @@ ibusattrs(DBusMessageIter *it) return 0; dbus_message_iter_get_basic(&st, &name); if(strcmp(name, "IBusAttrList") != 0 || !dbus_message_iter_next(&st) || - !emptyarray(&st, DBUS_TYPE_DICT_ENTRY) || - !dbus_message_iter_next(&st) || !emptyarray(&st, DBUS_TYPE_VARIANT) || - dbus_message_iter_next(&st)) + !emptyarray(&st, DBUS_TYPE_DICT_ENTRY) || !dbus_message_iter_next(&st) || + dbus_message_iter_get_arg_type(&st) != DBUS_TYPE_ARRAY || + dbus_message_iter_get_element_type(&st) != DBUS_TYPE_VARIANT) + return 0; + dbus_message_iter_recurse(&st, &a); + while(dbus_message_iter_get_arg_type(&a) != DBUS_TYPE_INVALID){ + if(!ibusattribute(&a, log)) + return 0; + dbus_message_iter_next(&a); + } + if(dbus_message_iter_next(&st)) return 0; return !dbus_message_iter_next(&v); } static int -ibustext(DBusMessageIter *it, char *text, size_t ntext) +ibustext(DBusMessageIter *it, char *text, size_t ntext, Attrlog *attrs) { DBusMessageIter v, st; const char *name, *s; @@ -540,7 +597,7 @@ ibustext(DBusMessageIter *it, char *text, size_t ntext) return 0; dbus_message_iter_get_basic(&st, &s); if(snprintf(text, ntext, "%s", s) >= (int)ntext || - !dbus_message_iter_next(&st) || !ibusattrs(&st) || + !dbus_message_iter_next(&st) || !ibusattrs(&st, attrs) || dbus_message_iter_next(&st)) return 0; return !dbus_message_iter_next(&v); @@ -560,13 +617,15 @@ onsignal(DBusConnection *conn, DBusMessage *m, void *arg) return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; if(dbus_message_has_member(m, "UpdatePreeditText")){ log->npreedit++; + log->nlegacy++; path = dbus_message_get_path(m); if(path == NULL || snprintf(log->preeditpath, sizeof log->preeditpath, "%s", path) >= (int)sizeof log->preeditpath || !dbus_message_has_signature(m, "vub") || !dbus_message_iter_init(m, &it) || - !ibustext(&it, log->preedit, sizeof log->preedit) || + !ibustext(&it, log->preedit, sizeof log->preedit, + &log->preeditattrs) || !dbus_message_iter_next(&it) || dbus_message_iter_get_arg_type(&it) != DBUS_TYPE_UINT32){ log->invalid = 1; @@ -583,6 +642,38 @@ onsignal(DBusConnection *conn, DBusMessage *m, void *arg) log->invalid = 1; return DBUS_HANDLER_RESULT_HANDLED; } + if(dbus_message_has_member(m, "UpdatePreeditTextWithMode")){ + log->npreedit++; + log->nmodern++; + path = dbus_message_get_path(m); + if(path == NULL || snprintf(log->preeditpath, + sizeof log->preeditpath, "%s", path) >= + (int)sizeof log->preeditpath || + !dbus_message_has_signature(m, "vubu") || + !dbus_message_iter_init(m, &it) || + !ibustext(&it, log->preedit, sizeof log->preedit, + &log->preeditattrs) || !dbus_message_iter_next(&it) || + dbus_message_iter_get_arg_type(&it) != DBUS_TYPE_UINT32){ + log->invalid = 1; + return DBUS_HANDLER_RESULT_HANDLED; + } + dbus_message_iter_get_basic(&it, &log->cursor); + if(!dbus_message_iter_next(&it) || + dbus_message_iter_get_arg_type(&it) != DBUS_TYPE_BOOLEAN){ + log->invalid = 1; + return DBUS_HANDLER_RESULT_HANDLED; + } + dbus_message_iter_get_basic(&it, &log->visible); + if(!dbus_message_iter_next(&it) || + dbus_message_iter_get_arg_type(&it) != DBUS_TYPE_UINT32){ + log->invalid = 1; + return DBUS_HANDLER_RESULT_HANDLED; + } + dbus_message_iter_get_basic(&it, &log->mode); + if(dbus_message_iter_next(&it)) + log->invalid = 1; + return DBUS_HANDLER_RESULT_HANDLED; + } if(dbus_message_has_member(m, "CommitText")){ log->ncommit++; path = dbus_message_get_path(m); @@ -591,7 +682,8 @@ onsignal(DBusConnection *conn, DBusMessage *m, void *arg) (int)sizeof log->commitpath || !dbus_message_has_signature(m, "v") || !dbus_message_iter_init(m, &it) || - !ibustext(&it, log->commit, sizeof log->commit) || + !ibustext(&it, log->commit, sizeof log->commit, + &log->commitattrs) || dbus_message_iter_next(&it)) log->invalid = 1; return DBUS_HANDLER_RESULT_HANDLED; @@ -903,6 +995,43 @@ contentcall(DBusConnection *conn, Siglog *log, char *path, return 1; } +static int +clientcommitcall(DBusConnection *conn, Siglog *log, char *path, int enabled) +{ + DBusMessage *m, *reply; + DBusMessageIter it, v, st; + const char *iface, *property; + dbus_bool_t b; + + resetsig(log); + iface = "org.freedesktop.IBus.InputContext"; + property = "ClientCommitPreedit"; + b = enabled ? TRUE : FALSE; + m = method(path, "org.freedesktop.DBus.Properties", "Set"); + if(m == NULL) + return 0; + dbus_message_iter_init_append(m, &it); + if(!dbus_message_iter_append_basic(&it, DBUS_TYPE_STRING, &iface) || + !dbus_message_iter_append_basic(&it, DBUS_TYPE_STRING, &property) || + !dbus_message_iter_open_container(&it, DBUS_TYPE_VARIANT, "(b)", &v) || + !dbus_message_iter_open_container(&v, DBUS_TYPE_STRUCT, NULL, &st) || + !dbus_message_iter_append_basic(&st, DBUS_TYPE_BOOLEAN, &b) || + !dbus_message_iter_close_container(&v, &st) || + !dbus_message_iter_close_container(&it, &v)){ + dbus_message_unref(m); + return fail("build Properties.Set ClientCommitPreedit call"); + } + reply = sendcall(conn, m); + if(reply == NULL) + return 0; + if(!dbus_message_has_signature(reply, "")){ + dbus_message_unref(reply); + return fail("Properties.Set ClientCommitPreedit returned a nonempty reply"); + } + dbus_message_unref(reply); + return 1; +} + static int keycall(DBusConnection *conn, Siglog *log, char *path, dbus_uint32_t sym, dbus_uint32_t state, int *eaten) @@ -1040,11 +1169,22 @@ nosignal(Siglog *log, char *where) return 1; } +static int +preeditattrs(Siglog *log) +{ + if(log->preedit[0] == '\0') + return log->preeditattrs.n == 0; + return log->preeditattrs.n == 1 && log->preeditattrs.type == 1 && + log->preeditattrs.value == 1 && log->preeditattrs.start == 0 && + log->preeditattrs.end == log->cursor; +} + static int preedit(Siglog *log, char *path, char *text, unsigned int cursor, int visible, char *where) { - if(log->invalid || log->npreedit != 1 || log->ncommit != 0 || + if(log->invalid || log->npreedit != 1 || log->nlegacy != 1 || + log->nmodern != 0 || log->ncommit != 0 || !preeditattrs(log) || strcmp(log->preeditpath, path) != 0 || strcmp(log->preedit, text) != 0 || log->cursor != cursor || (log->visible != FALSE) != (visible != 0)) @@ -1054,20 +1194,55 @@ preedit(Siglog *log, char *path, char *text, unsigned int cursor, int visible, return 1; } +static int +modernpreedit(Siglog *log, char *path, char *text, unsigned int cursor, + int visible, char *where) +{ + if(log->invalid || log->npreedit != 1 || log->nlegacy != 0 || + log->nmodern != 1 || log->ncommit != 0 || log->mode != 0 || + !preeditattrs(log) || strcmp(log->preeditpath, path) != 0 || + strcmp(log->preedit, text) != 0 || log->cursor != cursor || + (log->visible != FALSE) != (visible != 0)) + return fail("%s modern preedit: legacy=%d modern=%d commit=%d text=%s cursor=%u visible=%d mode=%u attrs=%d", + where, log->nlegacy, log->nmodern, log->ncommit, log->preedit, + (unsigned)log->cursor, log->visible != FALSE, + (unsigned)log->mode, log->preeditattrs.n); + return 1; +} + static int committed(Siglog *log, char *path, char *text, char *where) { - if(log->invalid || log->npreedit != 1 || log->ncommit != 1 || + if(log->invalid || log->npreedit != 1 || log->nlegacy != 1 || + log->nmodern != 0 || log->ncommit != 1 || strcmp(log->preeditpath, path) != 0 || strcmp(log->commitpath, path) != 0 || strcmp(log->commit, text) != 0 || strcmp(log->preedit, "") != 0 || log->cursor != 0 || + log->preeditattrs.n != 0 || log->commitattrs.n != 0 || log->visible != FALSE) return fail("%s signals: preedit=%d commit=%d text=%s/%s", where, log->npreedit, log->ncommit, log->commit, log->preedit); return 1; } +static int +moderncommitted(Siglog *log, char *path, char *text, char *where) +{ + if(log->invalid || log->npreedit != 1 || log->nlegacy != 0 || + log->nmodern != 1 || log->ncommit != 1 || log->mode != 0 || + strcmp(log->preeditpath, path) != 0 || + strcmp(log->commitpath, path) != 0 || strcmp(log->commit, text) != 0 || + strcmp(log->preedit, "") != 0 || log->cursor != 0 || + log->preeditattrs.n != 0 || log->commitattrs.n != 0 || + log->visible != FALSE) + return fail("%s modern signals: legacy=%d modern=%d commit=%d text=%s/%s attrs=%d/%d", + where, log->nlegacy, log->nmodern, log->ncommit, + log->commit, log->preedit, log->commitattrs.n, + log->preeditattrs.n); + return 1; +} + static int expectkey(DBusConnection *conn, Siglog *log, char *path, dbus_uint32_t sym, dbus_uint32_t state, int eaten, char *text, char *where) @@ -1370,6 +1545,30 @@ runpolicy(Daemon *d) !nosignal(&s1, "free-form property") || !expectkey(c1, &s1, a, 'n', 0, 1, "ん", "free-form resumes")) goto out; + + /* Modern clients receive only WithMode and code-point attribute ranges. */ + if(!emptycall(c1, &s1, a, "Reset") || + !preedit(&s1, a, "", 0, 0, "modern setup reset") || + !clientcommitcall(c1, &s1, a, 1) || + !nosignal(&s1, "enable client preedit commit") || + !keycall(c1, &s1, a, 'k', 0, &eaten) || !eaten || + !modernpreedit(&s1, a, "k", 1, 1, "modern key k") || + !keycall(c1, &s1, a, 'a', 0, &eaten) || !eaten || + !modernpreedit(&s1, a, "か", 1, 1, "modern key a") || + !keycall(c1, &s1, a, 'n', 0, &eaten) || !eaten || + !modernpreedit(&s1, a, "かん", 2, 1, "modern multibyte") || + !emptycall(c1, &s1, a, "Reset") || + !modernpreedit(&s1, a, "", 0, 0, "modern empty clear") || + !keycall(c1, &s1, a, 'k', 0, &eaten) || !eaten || + !modernpreedit(&s1, a, "k", 1, 1, "modern commit key k") || + !keycall(c1, &s1, a, 'a', 0, &eaten) || !eaten || + !modernpreedit(&s1, a, "か", 1, 1, "modern commit key a") || + !keycall(c1, &s1, a, '0', 0, &eaten) || !eaten || + !moderncommitted(&s1, a, "か", "modern commit") || + !clientcommitcall(c1, &s1, a, 0) || + !nosignal(&s1, "disable client preedit commit") || + !expectkey(c1, &s1, a, 'k', 0, 1, "k", "legacy restored")) + goto out; ok = 1; out: closebus(&c1, &s1); @@ -1377,14 +1576,61 @@ out: return ok; } +static int +runclient(Daemon *d, char *program) +{ + struct timespec pause; + pid_t pid, n; + int status; + int64_t deadline; + + pid = fork(); + if(pid < 0) + return fail("fork official libibus client: %s", strerror(errno)); + if(pid == 0){ + if(setenv("XDG_RUNTIME_DIR", d->runtime, 1) < 0 || + setenv("XDG_CONFIG_HOME", d->config, 1) < 0 || + setenv("HOME", d->home, 1) < 0) + _exit(126); + execl(program, program, d->address, (char*)0); + dprintf(STDERR_FILENO, "exec %s: %s\n", program, strerror(errno)); + _exit(127); + } + pause.tv_sec = 0; + pause.tv_nsec = 10000000; + deadline = nowms() + Starttimeout; + for(;;){ + n = waitpid(pid, &status, WNOHANG); + if(n == pid) + break; + if(n < 0 && errno == EINTR) + continue; + if(n < 0) + return fail("wait official libibus client: %s", strerror(errno)); + if(leftms(deadline) == 0){ + fail("official libibus client timed out"); + kill(pid, SIGKILL); + do + n = waitpid(pid, &status, 0); + while(n < 0 && errno == EINTR); + return 0; + } + nanosleep(&pause, NULL); + } + if(!WIFEXITED(status) || WEXITSTATUS(status) != 0) + return fail("official libibus client exited with wait status %#x", + status); + return 1; +} + int main(int argc, char **argv) { Daemon daemon; int ok; - if(argc != 3){ - fprintf(stderr, "usage: ibus_live_test strans mapdir\n"); + if(argc != 4){ + fprintf(stderr, "usage: ibus_live_test strans mapdir libibus-client\n"); return 2; } ok = startdaemon(&daemon, argv[1], argv[2]); @@ -1394,6 +1640,8 @@ main(int argc, char **argv) ok = runlifecycle(&daemon); if(ok) ok = runpolicy(&daemon); + if(ok) + ok = runclient(&daemon, argv[3]); if(!ok) showerrors(&daemon); if(!stopdaemon(&daemon))