diff --git a/Makefile b/Makefile index ed9304e..1ba96a2 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ DOCKER_IMAGE = strans-build DOCKER_RUN = docker run --rm --user "$$(id -u):$$(id -g)" \ -v "$(CURDIR):/src" $(DOCKER_IMAGE) -SRCS = dict.c font.c ibus.c ipc.c ko.c main.c popup_layout.c \ +SRCS = compose.c dict.c font.c ibus.c ipc.c ko.c main.c popup_layout.c \ srv.c str.c strans.c trie.c vi.c win.c xim.c OBJS = $(SRCS:.c=.o) @@ -29,7 +29,7 @@ $(PROG): $(OBJS) $(LD) $(LDFLAGS) -o $@ $(OBJS) $(PROJECT_LDLIBS) $(LDLIBS) $(OBJS): dat.h fn.h ipc.h -ibus.o: PROJECT_CPPFLAGS += $(IBUS_CFLAGS) +compose.o ibus.o: PROJECT_CPPFLAGS += $(IBUS_CFLAGS) font.o: PROJECT_CPPFLAGS += $(TEXT_CFLAGS) win.o: PROJECT_CPPFLAGS += $(POPUP_CFLAGS) xim.o: PROJECT_CPPFLAGS += $(XIM_CFLAGS) diff --git a/README.md b/README.md index 6367473..b0727e5 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,11 @@ a time. Leaving the field or clicking elsewhere commits what is pending. Each XIM style is offered with StatusNothing and with StatusNone. +Dead keys and Compose sequences are composed by strans itself for the XIM +and IBus frontends, from the table `XCOMPOSEFILE` or the locale names; the +GTK 3 module leaves them to GtkIMContextSimple. Their text follows +whatever was pending. + XIM text travels as `COMPOUND_TEXT`, which carries any UTF-8. A client that sends `XNSpotLocation` gets the popup under that spot whatever its preedit style, and above the line when there is no room below; otherwise the popup diff --git a/compose.c b/compose.c new file mode 100644 index 0000000..89d7611 --- /dev/null +++ b/compose.c @@ -0,0 +1,54 @@ +#include "dat.h" +#include "fn.h" + +#include +#include +#include + +static struct xkb_compose_state *cstate; + +/* Dead keys and Compose: no frontend's client does them for us. */ +void +composeinit(void) +{ + struct xkb_context *ctx; + struct xkb_compose_table *table; + char *locale; + + locale = setlocale(LC_CTYPE, ""); + ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS); + if(ctx == nil) + return; + table = xkb_compose_table_new_from_locale(ctx, + locale != nil ? locale : "C", XKB_COMPOSE_COMPILE_NO_FLAGS); + if(table != nil) + cstate = xkb_compose_state_new(table, XKB_COMPOSE_STATE_NO_FLAGS); + xkb_compose_table_unref(table); + xkb_context_unref(ctx); +} + +/* + * Feeds sym to the Compose table. Returns 1 while a sequence is + * unfinished: the key belongs to the sequence, not to the engine. A + * finished sequence leaves its text in buf, which the caller commits + * after whatever the engine had pending. + */ +int +composekey(u32int sym, char *buf, int n) +{ + buf[0] = '\0'; + if(cstate == nil || + xkb_compose_state_feed(cstate, sym) != XKB_COMPOSE_FEED_ACCEPTED) + return 0; + switch(xkb_compose_state_get_status(cstate)){ + case XKB_COMPOSE_COMPOSING: + case XKB_COMPOSE_CANCELLED: + return 1; + case XKB_COMPOSE_COMPOSED: + xkb_compose_state_get_utf8(cstate, buf, n); + break; + default: + break; + } + return 0; +} diff --git a/fn.h b/fn.h index 84cd3c3..300f31f 100644 --- a/fn.h +++ b/fn.h @@ -45,6 +45,9 @@ void ibusthread(void*); void ximthread(void*); int keymeaningful(u32int); +void composeinit(void); +int composekey(u32int, char*, int); + void* emalloc(ulong); void* erealloc(void*, ulong); diff --git a/ibus.c b/ibus.c index aa065bb..abd969f 100644 --- a/ibus.c +++ b/ibus.c @@ -531,13 +531,21 @@ dropconncontexts(DBusConnection *conn) * focus events of two applications can cross. */ static int -processkey(Ictx *ctx, u32int sym, u32int state, Keyres *res) +processkey(Ictx *ctx, u32int sym, u32int state, char *text, int n, Keyres *res) { + text[0] = '\0'; if(state & Relmask || hidden(ctx)) return 0; ctx->focused = 1; - sendrequest(ctx, Keypress, ipckeysym(sym, xkb_keysym_to_utf32(sym)), - ipcmod(state), res); + /* An IBus client drops a dead key we do not take: compose it here. */ + if(composekey(sym, text, n)) + return -1; + if(text[0] != '\0') + /* Composed text follows whatever was pending. */ + sendrequest(ctx, Keyreset, 0, 0, res); + else + sendrequest(ctx, Keypress, ipckeysym(sym, xkb_keysym_to_utf32(sym)), + ipcmod(state), res); if(preowner != nil && preowner != ctx) checkpreowner(); return 1; @@ -641,14 +649,15 @@ handlekey(DBusConnection *c, DBusMessage *m, Ictx *ctx) { dbus_uint32_t sym, code, state; Keyres res; - char commit[Maxutf], preedit[Maxutf]; - int restart; + char commit[Maxutf], preedit[Maxutf], text[Maxutf]; + int restart, rv; dbus_message_get_args(m, nil, DBUS_TYPE_UINT32, &sym, DBUS_TYPE_UINT32, &code, DBUS_TYPE_UINT32, &state, DBUS_TYPE_INVALID); - if(!processkey(ctx, sym, state, &res)) - return replybool(c, m, 0); + rv = processkey(ctx, sym, state, text, sizeof text, &res); + if(rv <= 0) /* below zero: a sequence in the making */ + return replybool(c, m, rv < 0); stoutf(&res.commit, commit, sizeof commit); stoutf(&res.preedit, preedit, sizeof preedit); /* A commit ends the preedit around it, so clients place it right. */ @@ -658,10 +667,12 @@ handlekey(DBusConnection *c, DBusMessage *m, Ictx *ctx) emitpreedit(ctx, ""); if(commit[0] != '\0') emitcommit(ctx, commit); + if(text[0] != '\0') + emitcommit(ctx, text); if(clientpreedit(ctx) && (!restart || preedit[0] != '\0')) emitpreedit(ctx, preedit); ctx->keying = 0; - return replybool(c, m, res.eaten); + return replybool(c, m, res.eaten || text[0] != '\0'); } /* Properties.Get PostProcessKeyEvent: what the last key produced. */ diff --git a/main.c b/main.c index ed53d25..2d4a759 100644 --- a/main.c +++ b/main.c @@ -55,6 +55,7 @@ threadmain(int argc, char **argv) drawc = chancreate(sizeof(Drawcmd), 4); keyc = chancreate(sizeof(Keyreq), 0); langinit(argv[1]); + composeinit(); srvinit(); proccreate(drawthread, nil, 16384); proccreate(srvthread, nil, 16384); diff --git a/tests/Makefile b/tests/Makefile index 05ec12e..ca2add4 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -32,10 +32,11 @@ FAULT = daemon_collision_test daemon_failure_test daemon_restart_test LIVE = $(SMOKE) $(FAULT) TESTSRC = test_util.c str_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 + popup_test.c font_test.c ibus_test.c server_test.c compose_test.c \ + xim_adapter_test.c TESTOBJ = $(TESTSRC:.c=.o) PARENTSRC = str.c trie.c dict.c ko.c vi.c ipc.c popup_layout.c \ - font.c + font.c compose.c PARENTOBJ = $(PARENTSRC:%.c=unit_%.o) COMMONOBJ = $(TESTOBJ) $(PARENTOBJ) OBJS = unit_test.o stress_test.o $(COMMONOBJ) @@ -125,6 +126,7 @@ xim_adapter_test.o: UNIT_CPPFLAGS += $(XIM_CFLAGS) xim_adapter_test.o: ../xim.c server_test.o: ../srv.c unit_font.o: UNIT_CPPFLAGS += $(TEXT_CFLAGS) +unit_compose.o compose_test.o: UNIT_CPPFLAGS += $(IBUS_CFLAGS) unit_%.o: ../%.c ../dat.h ../fn.h ../ipc.h $(CC) $(CPPFLAGS) $(UNIT_CPPFLAGS) $(UNIT_CFLAGS) $(CFLAGS) -c -o $@ $< diff --git a/tests/compose_test.c b/tests/compose_test.c new file mode 100644 index 0000000..76805cc --- /dev/null +++ b/tests/compose_test.c @@ -0,0 +1,37 @@ +#include "dat.h" +#include "fn.h" +#include "test.h" +#include + +/* A sequence swallows its keys and hands its text back at the end. */ +void +compose_sequences(struct ct *t) +{ + static const struct { + u32int sym; + int composing; + char *text; + } keys[] = { + { XKB_KEY_a, 0, "" }, + { XKB_KEY_dead_acute, 1, "" }, + { XKB_KEY_e, 0, "é" }, + { XKB_KEY_Multi_key, 1, "" }, + { XKB_KEY_o, 1, "" }, + { XKB_KEY_c, 0, "©" }, + { XKB_KEY_dead_acute, 1, "" }, + { XKB_KEY_Escape, 1, "" }, + { XKB_KEY_a, 0, "" }, + }; + char buf[Maxutf]; + int i; + + if(!CT_CHECK(t, setenv("XCOMPOSEFILE", "data/compose", 1) == 0)) + return; + composeinit(); + for(i = 0; i < nelem(keys); i++){ + CT_EQ_INT(t, keys[i].composing, + composekey(keys[i].sym, buf, sizeof buf)); + CT_EQ_STR(t, keys[i].text, buf); + } + unsetenv("XCOMPOSEFILE"); +} diff --git a/tests/data/compose b/tests/data/compose new file mode 100644 index 0000000..0afad5d --- /dev/null +++ b/tests/data/compose @@ -0,0 +1,2 @@ + : "é" + : "©" diff --git a/tests/ibus_test.c b/tests/ibus_test.c index ec4013b..17b46d1 100644 --- a/tests/ibus_test.c +++ b/tests/ibus_test.c @@ -153,9 +153,10 @@ static Keyres contextkey(struct ct *t, Ibusfix *f, Ictx *ctx, u32int sym, u32int state) { Keyres res; + char text[Maxutf]; memset(&res, 0, sizeof res); - CT_CHECK(t, processkey(ctx, sym, state, &res)); + CT_CHECK(t, processkey(ctx, sym, state, text, sizeof text, &res)); nexttrace(t, f, Keypress, ctx); return res; } @@ -219,6 +220,7 @@ ibus_private_input_policy(struct ct *t) Ibusfix f; Ictx *ctx; Keyres res; + char text[Maxutf]; int i; if(!ibusbegin(t, &f)) @@ -239,7 +241,7 @@ ibus_private_input_policy(struct ct *t) checkstr(t, "preedit", "k", &res.preedit); ctx->purpose = Ibuspurposepassword; memset(&res, 0, sizeof res); - CT_CHECK(t, !processkey(ctx, 'x', 0, &res)); + CT_CHECK(t, !processkey(ctx, 'x', 0, text, sizeof text, &res)); CT_CHECK(t, !res.eaten); notrace(t, &f); checkenginepreedit(t, "k"); @@ -270,6 +272,7 @@ ibus_context_lifecycle(struct ct *t) Keyreq req; Keyres res; Caret at; + char text[Maxutf]; if(!ibusbegin(t, &f)) goto cleanup; @@ -285,7 +288,7 @@ ibus_context_lifecycle(struct ct *t) notrace(t, &f); CT_CHECK(t, a->caret.valid); memset(&res, 0, sizeof res); - CT_CHECK(t, !processkey(a, 'x', Relmask, &res)); + CT_CHECK(t, !processkey(a, 'x', Relmask, text, sizeof text, &res)); CT_CHECK(t, !res.eaten); notrace(t, &f); CT_CHECK(t, !a->focused); @@ -293,7 +296,7 @@ ibus_context_lifecycle(struct ct *t) /* A key stands for the FocusIn a client may never send. */ memset(&res, 0, sizeof res); - CT_CHECK(t, processkey(a, 'x', 0, &res)); + CT_CHECK(t, processkey(a, 'x', 0, text, sizeof text, &res)); req = nexttrace(t, &f, Keypress, a); CT_CHECK(t, a->focused); CT_CHECK(t, !res.eaten); @@ -369,7 +372,7 @@ ibus_context_lifecycle(struct ct *t) notrace(t, &f); memset(&res, 0, sizeof res); - CT_CHECK(t, !processkey(b, 'x', Relmask, &res)); + CT_CHECK(t, !processkey(b, 'x', Relmask, text, sizeof text, &res)); CT_CHECK(t, !res.eaten); notrace(t, &f); CT_EQ_PTR(t, b, testengineowner()); diff --git a/tests/test.h b/tests/test.h index b021718..9b36d7e 100644 --- a/tests/test.h +++ b/tests/test.h @@ -112,6 +112,7 @@ void ibus_capability_policy(struct ct*); void ibus_private_input_policy(struct ct*); void ibus_context_lifecycle(struct ct*); void ibus_active_release_lifecycle(struct ct*); +void compose_sequences(struct ct*); void xim_keymap_lookup(struct ct*); void xim_compound_text(struct ct*); void xim_adapter_key_contract(struct ct*); diff --git a/tests/unit_test.c b/tests/unit_test.c index b59ccb3..dc1deb0 100644 --- a/tests/unit_test.c +++ b/tests/unit_test.c @@ -135,6 +135,7 @@ static const struct ct_test tests[] = { { "ibus/private-input-policy", ibus_private_input_policy }, { "ibus/context-lifecycle", ibus_context_lifecycle }, { "ibus/active-release-lifecycle", ibus_active_release_lifecycle }, + { "compose/sequences", compose_sequences }, { "xim/keymap-lookup", xim_keymap_lookup }, { "xim/compound-text", xim_compound_text }, { "xim/adapter-key-contract", xim_adapter_key_contract }, diff --git a/xim.c b/xim.c index 3228b1c..85e334a 100644 --- a/xim.c +++ b/xim.c @@ -8,7 +8,6 @@ #include #include #include -#include #include #include #include @@ -35,7 +34,6 @@ struct Ic static xcb_connection_t *conn; static xcb_im_t *xim; static struct xkb_state *kstate; -static struct xkb_compose_state *compose; static uint8_t xkbevent; static xcb_window_t rootwin; static Ic *ics; @@ -97,26 +95,6 @@ out: return ok; } -/* Dead keys and Compose: Xlib leaves them to the input method server. */ -static void -cinit(void) -{ - struct xkb_context *context; - struct xkb_compose_table *table; - char *locale; - - locale = setlocale(LC_CTYPE, ""); - context = xkb_context_new(XKB_CONTEXT_NO_FLAGS); - if(context == nil) - return; - table = xkb_compose_table_new_from_locale(context, - locale != nil ? locale : "C", XKB_COMPOSE_COMPILE_NO_FLAGS); - if(table != nil) - compose = xkb_compose_state_new(table, XKB_COMPOSE_STATE_NO_FLAGS); - xkb_compose_table_unref(table); - xkb_context_unref(context); -} - static void kwatch(void) { @@ -401,39 +379,27 @@ static void kpress(Ic *state, xcb_key_press_event_t *ev) { Keyres res; - char buf[Maxutf]; + char buf[Maxutf], text[Maxutf]; u32int key, sym; - int composed, n; + int n; sym = keymaplookup(kstate, ev->detail, ev->state); - composed = 0; - if(compose != nil && - xkb_compose_state_feed(compose, sym) == XKB_COMPOSE_FEED_ACCEPTED) - switch(xkb_compose_state_get_status(compose)){ - case XKB_COMPOSE_COMPOSING: - case XKB_COMPOSE_CANCELLED: - return; - case XKB_COMPOSE_COMPOSED: - sym = xkb_compose_state_get_one_sym(compose); - composed = 1; - break; - default: - break; - } + if(composekey(sym, text, sizeof text)) + return; key = ipckeysym(sym, xkb_keysym_to_utf32(sym)); if(keymeaningful(key)) place(state); - keypress(state, key, ev->state, &res); + if(text[0] != '\0') + /* Composed text follows whatever was pending. */ + sendrequest(state, Keyreset, 0, 0, &res); + else + keypress(state, key, ev->state, &res); n = stoutf(&res.commit, buf, sizeof buf); commit(state, buf, n); updatepreedit(state, &res.preedit); - if(res.eaten) - ; - else if(composed){ - /* The event holds the last key of the sequence, not its result. */ - n = xkb_compose_state_get_utf8(compose, buf, sizeof buf); - commit(state, buf, min(n, (int)sizeof buf - 1)); - }else + if(text[0] != '\0') + commit(state, text, strlen(text)); + else if(!res.eaten) xcb_im_forward_event(xim, state->xic, ev); xcb_flush(conn); } @@ -554,7 +520,6 @@ ximinit(void) ximlog("cannot read keyboard mapping"); return -1; } - cinit(); kwatch(); win = xcb_generate_id(conn); xcb_create_window(conn, XCB_COPY_FROM_PARENT, win, screen->root,