diff --git a/.gitignore b/.gitignore index a35daeb..ffd1480 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,6 @@ /tests/daemon_failure_test /tests/daemon_restart_test /xim/*.o -/xim/strans-xim /xim/xim_test /gtk/im-strans.so /bench/bench diff --git a/Makefile b/Makefile index 02335b3..812ca4b 100644 --- a/Makefile +++ b/Makefile @@ -2,8 +2,10 @@ CC = 9c LD = 9l PKG_CFLAGS = $(shell pkg-config --cflags dbus-1 xkbcommon) PKG_LIBS = $(shell pkg-config --libs dbus-1 xkbcommon) -TEXT_CFLAGS = $(shell pkg-config --cflags 'pangocairo >= 1.56' 'cairo >= 1.18') -TEXT_LIBS = $(shell pkg-config --libs 'pangocairo >= 1.56' 'cairo >= 1.18') +XIM_CFLAGS = $(shell pkg-config --cflags xcb-imdkit xkbcommon) +XIM_LIBS = $(shell pkg-config --libs xcb-imdkit xkbcommon) +TEXT_CFLAGS = $(shell pkg-config --cflags pangocairo cairo) +TEXT_LIBS = $(shell pkg-config --libs pangocairo cairo) CFLAGS = -Wall -Wextra -O2 -g $(PKG_CFLAGS) PROG = strans DOCKER_IMAGE = strans-build @@ -12,15 +14,19 @@ DOCKER_RUN = docker run --rm --user "$$(id -u):$$(id -g)" \ SRCS = dict.c font.c hash.c ibus.c ipc.c ko.c main.c popup_layout.c \ srv.c str.c strans.c trie.c vi.c win.c -OBJS = $(SRCS:.c=.o) +XIMSRCS = xim/xim.c xim/keymap.c xim/ximtext.c +XIMOBJS = $(XIMSRCS:.c=.o) +OBJS = $(SRCS:.c=.o) $(XIMOBJS) -all: $(PROG) xim gtk +all: $(PROG) gtk $(PROG): $(OBJS) - $(LD) -o $@ $(OBJS) -lthread -lbio -lxcb $(PKG_LIBS) $(TEXT_LIBS) + $(LD) -o $@ $(OBJS) -lthread -lbio -lxcb $(PKG_LIBS) $(TEXT_LIBS) \ + $(XIM_LIBS) $(OBJS): dat.h fn.h ipc.h font.o: CFLAGS += $(TEXT_CFLAGS) +$(XIMOBJS): CFLAGS += -I. $(XIM_CFLAGS) clean: rm -f $(OBJS) $(PROG) @@ -29,9 +35,6 @@ clean: $(MAKE) -C gtk/ clean $(MAKE) -C bench/ clean -xim: - $(MAKE) -C xim/ - gtk: $(MAKE) -C gtk/ @@ -68,5 +71,5 @@ verify-map: python3 -B tests/mkhanja_test.py python3 -B tests/skk2ktrans_test.py -.PHONY: all check test verify-map clean xim gtk bench docker docker-image \ +.PHONY: all check test verify-map clean gtk bench docker docker-image \ docker-build docker-check docker-bench diff --git a/README.md b/README.md index edc9e9d..2aa4496 100644 --- a/README.md +++ b/README.md @@ -40,9 +40,8 @@ make docker-check Build output: ```text -strans daemon and IBus frontend -xim/strans-xim XIM frontend -gtk/im-strans.so GTK 3 frontend +strans daemon with IBus and XIM frontends +gtk/im-strans.so GTK 3 frontend module ``` Use `make docker-check TESTARGS=hangul` to run selected C tests. Native @@ -57,16 +56,16 @@ from the build tree: ```sh ./run.sh -# daemon only +# equivalently ./strans map ``` Pango shapes the complete string and selects glyph fallback from the system font set. -The popup renderer requires Pango/PangoCairo 1.56 or newer and Cairo 1.18 or -newer. `run.sh` starts the daemon and, when `DISPLAY` is set, the XIM frontend. -It restarts existing `strans` processes owned by the current user. +The popup renderer uses Pango/PangoCairo and Cairo. When `DISPLAY` is set, +`strans` starts its XIM worker in the same process. `run.sh` restarts the +existing `strans` process owned by the current user. Configure clients as needed: @@ -82,8 +81,8 @@ GLFW_IM_MODULE=ibus kitty ``` strans provides its own IBus endpoint; `ibus-daemon` and fcitx are not -required. Without `DISPLAY`, the daemon and IBus frontend still work, but the -popup is disabled. +required. Without `DISPLAY`, the daemon and IBus frontend still work, but XIM +is not started and the popup is disabled. ## Dictionary data diff --git a/fn.h b/fn.h index d9cbcda..e0f9658 100644 --- a/fn.h +++ b/fn.h @@ -39,6 +39,7 @@ void backvi(Im*); void srvinit(void); void srvthread(void*); void ibusthread(void*); +void ximthread(void*); void* emalloc(ulong); void* erealloc(void*, ulong); diff --git a/main.c b/main.c index 0de1751..f5b8fe2 100644 --- a/main.c +++ b/main.c @@ -49,6 +49,8 @@ erealloc(void *p, ulong n) void threadmain(int argc, char **argv) { + char *display; + if(argc != 2) usage(); @@ -65,6 +67,10 @@ threadmain(int argc, char **argv) die("can't create server worker"); if(proccreate(ibusthread, nil, 32768) < 0) die("can't create IBus worker"); + display = getenv("DISPLAY"); + if(display != nil && display[0] != '\0' && + proccreate(ximthread, nil, 32768) < 0) + fprint(2, "strans: xim: cannot create worker\n"); if(threadcreate(dictthread, nil, 16384) < 0) die("can't create dictionary worker"); imthread(nil); diff --git a/run.sh b/run.sh index cd3a7eb..8da97c9 100755 --- a/run.sh +++ b/run.sh @@ -11,28 +11,16 @@ if ! test -x ./strans; then echo "run.sh: ./strans is not executable; run make first" >&2 exit 1 fi -if test -n "${DISPLAY-}" && ! test -x xim/strans-xim; then - echo "run.sh: xim/strans-xim is not executable; run make first" >&2 - exit 1 -fi if ! command -v pkill >/dev/null 2>&1; then echo "run.sh: pkill is required" >&2 exit 1 fi uid=$(id -u) || exit 1 -pkill -TERM -u "$uid" -x strans-xim 2>/dev/null || : pkill -TERM -u "$uid" -x strans 2>/dev/null || : sleep 0.1 -pkill -KILL -u "$uid" -x strans-xim 2>/dev/null || : pkill -KILL -u "$uid" -x strans 2>/dev/null || : ./strans map -#include -#include -#include -#include -#include -#include -#include -#include "ipc.h" - -uint32_t keymaplookup(const uint32_t*, int, uint16_t); -char *ximcompound(const char*, size_t, size_t*); - -typedef struct Ic Ic; -struct Ic -{ - int fd; -}; - -static xcb_connection_t *conn; -static xcb_im_t *xim; -static xcb_keysym_t *kmap; -static uint8_t minkc, maxkc; -static uint8_t symsper; - -static char *encs[] = {"COMPOUND_TEXT"}; -static uint32_t styles[] = { - XCB_IM_PreeditNothing | XCB_IM_StatusNothing, - XCB_IM_PreeditNone | XCB_IM_StatusNone, -}; - -static void -die(char *msg) -{ - fprintf(stderr, "strans-xim: %s\n", msg); - exit(1); -} - -static void -kinit(void) -{ - xcb_get_keyboard_mapping_cookie_t c; - xcb_get_keyboard_mapping_reply_t *r; - const xcb_setup_t *setup; - xcb_keysym_t *syms; - int n; - - setup = xcb_get_setup(conn); - if(setup == NULL) - die("xcb_get_setup failed"); - minkc = setup->min_keycode; - maxkc = setup->max_keycode; - c = xcb_get_keyboard_mapping(conn, minkc, maxkc - minkc + 1); - r = xcb_get_keyboard_mapping_reply(conn, c, NULL); - if(r == NULL) - die("keyboard mapping failed"); - symsper = r->keysyms_per_keycode; - n = xcb_get_keyboard_mapping_keysyms_length(r); - syms = malloc(n * sizeof(xcb_keysym_t)); - if(syms == NULL) - die("malloc failed"); - memcpy(syms, xcb_get_keyboard_mapping_keysyms(r), - n * sizeof(xcb_keysym_t)); - free(kmap); - kmap = syms; - free(r); -} - -static uint32_t -kget(uint8_t kc, uint16_t state) -{ - if(kmap == NULL || kc < minkc || kc > maxkc) - return 0; - return keymaplookup(kmap + (kc - minkc) * symsper, symsper, state); -} - -static xcb_screen_t* -getscreen(int scr) -{ - xcb_screen_iterator_t iter; - const xcb_setup_t *setup; - - setup = xcb_get_setup(conn); - if(setup == NULL) - die("xcb_get_setup failed"); - iter = xcb_setup_roots_iterator(setup); - for(; iter.rem; scr--, xcb_screen_next(&iter)) - if(scr == 0) - return iter.data; - die("no screen"); - return NULL; -} - -static void -commit(xcb_im_input_context_t *ic, char *s, int len) -{ - char *ct; - size_t clen; - - if(len == 0) - return; - ct = ximcompound(s, len, &clen); - if(ct == NULL) - return; - xcb_im_commit_string(xim, ic, XCB_XIM_LOOKUP_CHARS, ct, clen, 0); - xcb_flush(conn); - free(ct); -} - -static void -srvclose(Ic *state) -{ - if(state == NULL || state->fd < 0) - return; - close(state->fd); - state->fd = -1; -} - -static int -srvconnect(Ic *state) -{ - if(state->fd >= 0) - return 0; - state->fd = ipcconnect(); - if(state->fd < 0) - return -1; - return 0; -} - -static int -readresp(Ic *state, xcb_im_input_context_t *ic) -{ - char buf[Ipcfieldmax+1]; - Ipcresp resp; - - if(ipcreadresp(state->fd, 0, buf, sizeof buf, NULL, 0, &resp) < 0) - return -1; - if(resp.ncommit > 0) - commit(ic, buf, resp.ncommit); - return resp.eaten; -} - -static void -kpress(Ic *state, xcb_im_input_context_t *ic, xcb_key_press_event_t *ev) -{ - unsigned char buf[Ipcreqsz]; - uint32_t key, rune; - int eaten; - - key = kget(ev->detail, ev->state); - rune = xkb_keysym_to_utf32(key); - if(rune >= ' ' && rune != 0x7f) - key = rune; - else if(key >= 0xff00 && key <= 0xffff) - key = Kspec + (key - 0xff00); - else - key = rune; - ipcpackreq(buf, 0, ev->state, key); - eaten = 0; - if(srvconnect(state) == 0){ - if(ipcsend(state->fd, buf, sizeof buf) < 0 || - (eaten = readresp(state, ic)) < 0){ - srvclose(state); - eaten = 0; - } - } - if(eaten == 0) - xcb_im_forward_event(xim, ic, ev); - xcb_flush(conn); -} - -static void -reset(Ic *state, xcb_im_input_context_t *ic, int release) -{ - unsigned char buf[Ipcreqsz]; - - if(state == NULL || state->fd < 0) - return; - ipcpackreset(buf, 0); - if(ipcsend(state->fd, buf, sizeof buf) < 0 || - readresp(state, ic) < 0) - release = 1; - if(release) - srvclose(state); -} - -static void -icfree(void *p) -{ - Ic *state; - - state = p; - /* imdkit frees this data on context destruction and client loss. */ - srvclose(state); - free(state); -} - -static void -iccreate(xcb_im_input_context_t *ic) -{ - Ic *state; - - if(ic == NULL || xcb_im_input_context_get_data(ic) != NULL) - return; - state = calloc(1, sizeof *state); - if(state == NULL) - return; - state->fd = -1; - xcb_im_input_context_set_data(ic, state, icfree); -} - -static void -callback(xcb_im_t *im, xcb_im_client_t *client, xcb_im_input_context_t *ic, - const xcb_im_packet_header_fr_t *hdr, void *frame, void *arg, void *user) -{ - xcb_key_press_event_t *ev; - Ic *state; - - (void)im; - (void)client; - (void)frame; - (void)user; - - if(hdr->major_opcode == XCB_XIM_CREATE_IC){ - iccreate(ic); - return; - } - if(ic == NULL) - return; - state = xcb_im_input_context_get_data(ic); - if(state == NULL) - return; - switch(hdr->major_opcode){ - case XCB_XIM_FORWARD_EVENT: - ev = arg; - if(ev != NULL && (ev->response_type & ~0x80) == XCB_KEY_PRESS) - kpress(state, ic, ev); - break; - case XCB_XIM_RESET_IC: - /* ResetIC ends this context's ownership, like focus loss. */ - reset(state, ic, 1); - break; - case XCB_XIM_UNSET_IC_FOCUS: - reset(state, ic, 1); - break; - } -} - -static void -ximinit(void) -{ - xcb_screen_t *screen; - xcb_window_t win; - xcb_im_styles_t st; - xcb_im_encodings_t enc; - int scr; - - st.nStyles = 2; - st.styles = styles; - enc.nEncodings = 1; - enc.encodings = encs; - xcb_compound_text_init(); - conn = xcb_connect(NULL, &scr); - if(conn == NULL || xcb_connection_has_error(conn)) - die("xcb_connect failed"); - screen = getscreen(scr); - kinit(); - win = xcb_generate_id(conn); - xcb_create_window(conn, XCB_COPY_FROM_PARENT, win, screen->root, - 0, 0, 1, 1, 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, - screen->root_visual, 0, NULL); - xim = xcb_im_create(conn, scr, win, "strans", - XCB_IM_ALL_LOCALES, &st, NULL, NULL, &enc, - XCB_EVENT_MASK_KEY_PRESS, callback, NULL); - if(xim == NULL || !xcb_im_open_im(xim)) - die("xcb_im failed"); -} - -int -main(void) -{ - xcb_generic_event_t *ev; - uint8_t type; - - ximinit(); - for(;;){ - ev = xcb_wait_for_event(conn); - if(ev == NULL) - break; - type = ev->response_type & ~0x80; - if(type == XCB_MAPPING_NOTIFY) - kinit(); - else - xcb_im_filter_event(xim, ev); - free(ev); - } - return 0; -} diff --git a/xim/xim.c b/xim/xim.c new file mode 100644 index 0000000..d8b4f55 --- /dev/null +++ b/xim/xim.c @@ -0,0 +1,364 @@ +#include "dat.h" +#include "fn.h" + +#include +#include +#include +#include + +u32int keymaplookup(const u32int*, int, u16int); +char *ximcompound(const char*, size_t, size_t*); + +typedef struct Ic Ic; +struct Ic +{ + Ic *next; + int engaged; +}; + +static xcb_connection_t *conn; +static xcb_im_t *xim; +static xcb_keysym_t *kmap; +static uint8_t minkc, maxkc; +static uint8_t symsper; +static int opened; +static Ic *ics; +static Channel *replyc; + +static char *encs[] = {"COMPOUND_TEXT"}; +static u32int styles[] = { + XCB_IM_PreeditNothing | XCB_IM_StatusNothing, + XCB_IM_PreeditNone | XCB_IM_StatusNone, +}; + +static void +ximlog(char *msg) +{ + fprint(2, "strans: xim: %s\n", msg); +} + +static int +kinit(void) +{ + xcb_get_keyboard_mapping_cookie_t c; + xcb_get_keyboard_mapping_reply_t *r; + const xcb_setup_t *setup; + xcb_keysym_t *syms; + uint8_t newmin, newmax, newper; + int n; + + setup = xcb_get_setup(conn); + if(setup == nil) + return -1; + newmin = setup->min_keycode; + newmax = setup->max_keycode; + c = xcb_get_keyboard_mapping(conn, newmin, newmax - newmin + 1); + r = xcb_get_keyboard_mapping_reply(conn, c, nil); + if(r == nil) + return -1; + newper = r->keysyms_per_keycode; + n = xcb_get_keyboard_mapping_keysyms_length(r); + syms = malloc(n * sizeof(xcb_keysym_t)); + if(syms == nil){ + free(r); + return -1; + } + memcpy(syms, xcb_get_keyboard_mapping_keysyms(r), + n * sizeof(xcb_keysym_t)); + free(kmap); + kmap = syms; + minkc = newmin; + maxkc = newmax; + symsper = newper; + free(r); + return 0; +} + +static u32int +kget(uint8_t kc, u16int state) +{ + if(kmap == nil || kc < minkc || kc > maxkc) + return 0; + return keymaplookup(kmap + (kc - minkc) * symsper, symsper, state); +} + +static xcb_screen_t* +getscreen(int scr) +{ + xcb_screen_iterator_t iter; + const xcb_setup_t *setup; + + setup = xcb_get_setup(conn); + if(setup == nil) + return nil; + iter = xcb_setup_roots_iterator(setup); + for(; iter.rem; scr--, xcb_screen_next(&iter)) + if(scr == 0) + return iter.data; + return nil; +} + +static void +commit(xcb_im_input_context_t *ic, char *s, int len) +{ + char *ct; + size_t clen; + + if(len == 0) + return; + ct = ximcompound(s, len, &clen); + if(ct == nil) + return; + xcb_im_commit_string(xim, ic, XCB_XIM_LOOKUP_CHARS, ct, clen, 0); + xcb_flush(conn); + free(ct); +} + +static int +meaningful(u32int key) +{ + return key != 0 && (key < Kmodfirst || key > Kmodlast); +} + +static void +sendrequest(Ic *state, int op, u32int key, u32int mod, Keyres *res) +{ + Keyreq kr; + + memset(&kr, 0, sizeof kr); + kr.owner = state; + kr.op = op; + kr.ks = key; + kr.mod = mod & Mmask; + kr.reply = replyc; + chansend(keyc, &kr); + chanrecv(replyc, res); +} + +static void +keypress(Ic *state, u32int key, u32int mod, Keyres *res) +{ + if(meaningful(key)) + state->engaged = 1; + sendrequest(state, Keypress, key, mod, res); +} + +static void +release(Ic *state) +{ + Keyres res; + + if(state == nil || !state->engaged) + return; + sendrequest(state, Keyrelease, 0, 0, &res); + state->engaged = 0; +} + +static void +kpress(Ic *state, xcb_im_input_context_t *ic, xcb_key_press_event_t *ev) +{ + Keyres res; + char buf[Maxutf]; + u32int key, rune; + int n; + + key = kget(ev->detail, ev->state); + rune = xkb_keysym_to_utf32(key); + if(rune >= ' ' && rune != 0x7f) + key = rune; + else if(key >= 0xff00 && key <= 0xffff) + key = Kspec + (key - 0xff00); + else + key = rune; + keypress(state, key, ev->state, &res); + n = stoutf(&res.commit, buf, sizeof buf); + commit(ic, buf, n); + if(!res.eaten) + xcb_im_forward_event(xim, ic, ev); + xcb_flush(conn); +} + +static void +icunlink(Ic *state) +{ + Ic **p; + + for(p = &ics; *p != nil; p = &(*p)->next) + if(*p == state){ + *p = state->next; + return; + } +} + +static void +icfree(void *p) +{ + Ic *state; + + state = p; + /* The owner remains valid until imthread acknowledges its release. */ + release(state); + icunlink(state); + free(state); +} + +static void +iccreate(xcb_im_input_context_t *ic) +{ + Ic *state; + + if(ic == nil || xcb_im_input_context_get_data(ic) != nil) + return; + state = calloc(1, sizeof *state); + if(state == nil) + return; + state->next = ics; + ics = state; + xcb_im_input_context_set_data(ic, state, icfree); +} + +static void +callback(xcb_im_t *im, xcb_im_client_t *client, xcb_im_input_context_t *ic, + const xcb_im_packet_header_fr_t *hdr, void *frame, void *arg, void *user) +{ + xcb_key_press_event_t *ev; + Ic *state; + + USED(im); + USED(client); + USED(frame); + USED(user); + + if(hdr->major_opcode == XCB_XIM_CREATE_IC){ + iccreate(ic); + return; + } + if(ic == nil) + return; + state = xcb_im_input_context_get_data(ic); + if(state == nil) + return; + switch(hdr->major_opcode){ + case XCB_XIM_FORWARD_EVENT: + ev = arg; + if(ev != nil && (ev->response_type & ~0x80) == XCB_KEY_PRESS) + kpress(state, ic, ev); + break; + case XCB_XIM_RESET_IC: + case XCB_XIM_UNSET_IC_FOCUS: + release(state); + break; + } +} + +static void +ximclose(void) +{ + Ic *state, *next; + + for(state = ics; state != nil; state = state->next) + release(state); + if(xim != nil){ + if(opened) + xcb_im_close_im(xim); + xcb_im_destroy(xim); + xim = nil; + } + opened = 0; + for(state = ics; state != nil; state = next){ + next = state->next; + free(state); + } + ics = nil; + free(kmap); + kmap = nil; + if(conn != nil){ + xcb_disconnect(conn); + conn = nil; + } + if(replyc != nil){ + chanfree(replyc); + replyc = nil; + } +} + +static int +ximinit(void) +{ + xcb_screen_t *screen; + xcb_window_t win; + xcb_im_styles_t st; + xcb_im_encodings_t enc; + int scr; + + replyc = chancreate(sizeof(Keyres), 0); + if(replyc == nil){ + ximlog("cannot create reply channel"); + return -1; + } + st.nStyles = nelem(styles); + st.styles = styles; + enc.nEncodings = nelem(encs); + enc.encodings = encs; + xcb_compound_text_init(); + conn = xcb_connect(nil, &scr); + if(conn == nil || xcb_connection_has_error(conn)){ + ximlog("cannot connect to X server"); + return -1; + } + screen = getscreen(scr); + if(screen == nil){ + ximlog("cannot find X screen"); + return -1; + } + if(kinit() < 0){ + ximlog("cannot read keyboard mapping"); + return -1; + } + win = xcb_generate_id(conn); + xcb_create_window(conn, XCB_COPY_FROM_PARENT, win, screen->root, + 0, 0, 1, 1, 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, + screen->root_visual, 0, nil); + xim = xcb_im_create(conn, scr, win, "strans", + XCB_IM_ALL_LOCALES, &st, nil, nil, &enc, + XCB_EVENT_MASK_KEY_PRESS, callback, nil); + if(xim == nil){ + ximlog("cannot create XIM server"); + return -1; + } + if(!xcb_im_open_im(xim)){ + ximlog("cannot claim XIM selection"); + return -1; + } + opened = 1; + return 0; +} + +void +ximthread(void *arg) +{ + xcb_generic_event_t *ev; + uint8_t type; + + USED(arg); + threadsetname("xim"); + if(ximinit() < 0){ + ximclose(); + return; + } + for(;;){ + ev = xcb_wait_for_event(conn); + if(ev == nil) + break; + type = ev->response_type & ~0x80; + if(type == XCB_MAPPING_NOTIFY){ + if(kinit() < 0) + ximlog("cannot refresh keyboard mapping"); + }else + xcb_im_filter_event(xim, ev); + free(ev); + } + if(xcb_connection_has_error(conn)) + ximlog("X server disconnected"); + ximclose(); +}