#include "dat.h" #include "fn.h" static Im im; static int visible = 0; static void *activeowner; static int activecap; static Caret caret; static int candidatechosen; static Emit transjp(Im*, Rune); static void backjp(Im*); static int maplookup(Trie*, Str*, Str*); typedef struct Search Search; struct Search { int lang; Str raw; Str text; }; static Search search; Lang langs[] = { {LangEN, nil, nil, nil, nil, nil, nil}, {LangJP, "hira", "kanji", transjp, backjp, nil, nil}, {LangJPK, "kata", nil, transjp, backjp, nil, nil}, {LangKO, nil, nil, transko, backko, nil, nil}, {LangHANJA, nil, "hanja", nil, nil, nil, nil}, {LangEMOJI, nil, "emoji", nil, nil, nil, nil}, {LangVI, "telex", nil, transvi, backvi, nil, nil}, }; int nlang = nelem(langs); static void clearkouho(void) { im.nkouho = 0; im.sel = -1; candidatechosen = 0; } static void selectfirst(void) { im.sel = im.nkouho > 0 ? 0 : -1; } static int pagefirst(void) { if(im.sel < 0) return 0; return im.sel / Maxdisp * Maxdisp; } static int movedelta(u32int ks) { static struct { u32int key; int delta; } move[] = { {Kup, -1}, {Kdown, 1}, {Kpgup, -Maxdisp}, {Kpgdown, Maxdisp}, }; int i; for(i = 0; i < nelem(move); i++) if(move[i].key == ks) return move[i].delta; return 0; } static void movekouho(int delta) { if(im.sel < 0) im.sel = 0; else im.sel += delta; if(im.sel < 0) im.sel = 0; if(im.sel >= im.nkouho) im.sel = im.nkouho - 1; candidatechosen = 1; } static int numberedkouho(u32int ks, u32int mod) { int n; if(mod != 0 || ks < '1' || ks > '9') return -1; n = pagefirst() + ks - '1'; if(n >= im.nkouho) return -1; return n; } static int isjp(Im *p) { return p->l->lang == LangJP || p->l->lang == LangJPK; } /* * Japanese keeps completed kana in pre and an ambiguous romaji syllable in * raw. A mapped raw suffix is part of the reading even while it remains * pending (notably n, which may still grow into nya). Returns whether * the reading is complete. */ static int jpreading(Trie *map, Str *pre, Str *raw, Str *s) { Str mapped; *s = *pre; if(raw->n == 0) return 1; if(mapget(map, raw, &mapped)){ sappend(s, &mapped); return 1; } sappend(s, raw); return 0; } static int haspre(Im *p) { return p->pre.n != 0 || p->raw.n != 0; } /* The preedit as clients see it: Telex keys read back through the map. */ static void impre(Str *s) { if(isjp(&im)) jpreading(im.l->map, &im.pre, &im.raw, s); else if(!mapget(im.l->map, &im.pre, s)) *s = im.pre; } static void snapshot(Drawcmd *dc) { Str pre; int i, first, n; memset(dc, 0, sizeof *dc); if(search.lang) pre = search.text; else impre(&pre); if(!(activecap & Cclientpreedit)) dc->pre = pre; first = pagefirst(); n = im.nkouho - first; if(n > Maxdisp) n = Maxdisp; dc->nkouho = n; dc->sel = im.sel >= 0 ? im.sel - first : -1; dc->first = first; dc->total = im.nkouho; dc->caret = caret; for(i = 0; i < n; i++) dc->kouho[i] = im.kouho[first + i]; } static void redraw(void) { static Drawcmd old; Drawcmd dc; snapshot(&dc); if(dc.pre.n == 0 && dc.nkouho == 0 && !visible) return; visible = dc.pre.n != 0 || dc.nkouho != 0; /* imthread is the sole producer; keep only the newest pending draw. */ while(channbrecv(drawc, &old) > 0) ; channbsend(drawc, &dc); } static void reset(void) { sclear(&im.pre); sclear(&im.raw); search.lang = 0; sclear(&search.raw); sclear(&search.text); memset(&caret, 0, sizeof caret); clearkouho(); } /* Kanji candidates for a complete reading. */ static void dictqjp(void) { Str reading; clearkouho(); if(!jpreading(im.l->map, &im.pre, &im.raw, &reading) || reading.n == 0) return; im.nkouho = dictlookup(im.l, &reading, im.kouho, Maxkouho); selectfirst(); } static int setlang(int c) { Lang *l; l = getlang(c); if(l == nil) return 0; im.l = l; return 1; } static void commitim(Im *p, Str *com) { Str val; if(isjp(p)) jpreading(p->l->map, &p->pre, &p->raw, &val); else if(!mapget(p->l->map, &p->pre, &val)) val = p->pre; sappend(com, &val); sclear(&p->pre); sclear(&p->raw); } static void commit(Str *com) { commitim(&im, com); } static void backjp(Im *im) { if(im->raw.n != 0) spopr(&im->raw); else spopr(&im->pre); } static int issmalltsu(Str *s) { return s->n == 1 && (s->r[0] == L'っ' || s->r[0] == L'ッ'); } static void keeptail(Str *s) { int i; for(i = 1; i < s->n; i++) s->r[i-1] = s->r[i]; if(s->n > 0) s->n--; } static Emit transjp(Im *p, Rune c) { Emit e; Str key, mapped; memset(&e, 0, sizeof e); e.eat = 1; e.next = p->pre; e.raw = p->raw; if(c == '\'' && e.raw.n == 1 && e.raw.r[0] == 'n'){ if(mapget(p->l->map, &e.raw, &mapped)) sappend(&e.next, &mapped); sclear(&e.raw); return e; } key = e.raw; sputr(&key, c); if(maplookup(p->l->map, &key, &mapped)){ e.raw = key; if(issmalltsu(&mapped)){ sappend(&e.next, &mapped); keeptail(&e.raw); } return e; } if(e.raw.n != 0 && mapget(p->l->map, &e.raw, &mapped)){ sappend(&e.next, &mapped); sclear(&e.raw); sclear(&key); sputr(&key, c); if(maplookup(p->l->map, &key, &mapped)){ e.raw = key; return e; } } /* The key starts no syllable: flush the reading and pass it on. */ jpreading(p->l->map, &e.next, &e.raw, &e.s); sclear(&e.next); sclear(&e.raw); e.eat = 0; return e; } static int dotrans(Rune c, Str *com) { Emit e; e = im.l->trans(&im, c); sappend(com, &e.s); im.pre = e.next; im.raw = e.raw; if(e.eat && isjp(&im)) dictqjp(); else clearkouho(); return e.eat; } Lang* getlang(int lang) { int i; for(i = 0; i < nelem(langs); i++) if(langs[i].lang == lang) return &langs[i]; return nil; } static int maplookup(Trie *t, Str *key, Str *out) { char *v; int match, vlen; sclear(out); if(key->n == 0) return 0; match = trielookup(t, key, &v, &vlen); if(match == TrieMiss) return 0; if(match == TrieExact && !sinit(out, v, vlen)) return 0; return 1; } Emit transmap(Im *im, Rune c) { Emit e = {0}; Str key, mapped; Trie *t; t = im->l->map; key = im->pre; sputr(&key, c); if(maplookup(t, &key, &mapped)){ e.eat = 1; e.next = key; return e; } if(!mapget(t, &im->pre, &e.s)) e.s = im->pre; sclear(&key); sputr(&key, c); if(!maplookup(t, &key, &mapped)) return e; e.eat = 1; sputr(&e.next, c); return e; } static Rune ctrlkey(Rune c) { if(c >= 'A' && c <= 'Z') c += 'a' - 'A'; if(c >= 'a' && c <= 'z') return c - 'a' + 1; return c; } static Rune kokey(Rune c, u32int mod) { if(c >= 'A' && c <= 'Z') c += 'a' - 'A'; if((mod & Mshift) && c >= 'a' && c <= 'z') c -= 'a' - 'A'; return c; } static int ismodkey(u32int ks) { return ks >= Kmodfirst && ks <= Kmodlast; } static void foldascii(Str *s) { int i; for(i = 0; i < s->n; i++) if(s->r[i] >= 'A' && s->r[i] <= 'Z') s->r[i] += 'a' - 'A'; } /* What typing raw in language l would produce, composition included. */ static void transstr(Lang *l, Str *raw, Str *out) { Im q; Emit e; int i; sclear(out); if(l->trans == nil){ *out = *raw; return; } memset(&q, 0, sizeof q); q.l = l; for(i = 0; i < raw->n; i++){ e = l->trans(&q, raw->r[i]); sappend(out, &e.s); q.pre = e.next; q.raw = e.raw; if(!e.eat) sputr(out, raw->r[i]); } commitim(&q, out); } static void addkouho(Str *s) { int i; for(i = 0; i < im.nkouho; i++) if(scmp(&im.kouho[i], s) == 0) return; if(im.nkouho < Maxkouho) im.kouho[im.nkouho++] = *s; } /* The typed keys and their transliteration are both emoji queries. */ static void emojiquery(void) { Str kouho[Maxkouho], raw, local; int i, n, rawhit, localhit; raw = search.raw; foldascii(&raw); transstr(im.l, &search.raw, &local); foldascii(&local); clearkouho(); n = dictlookup(getlang(LangEMOJI), &raw, kouho, Maxkouho); rawhit = n != 0; for(i = 0; i < n; i++) addkouho(&kouho[i]); localhit = 0; if(scmp(&raw, &local) != 0){ n = dictlookup(getlang(LangEMOJI), &local, kouho, Maxkouho); localhit = n != 0; for(i = 0; i < n; i++) addkouho(&kouho[i]); } search.text = rawhit || !localhit ? raw : local; selectfirst(); } static void hanjaquery(void) { Str key; transstr(im.l, &search.raw, &key); search.text = key; clearkouho(); im.nkouho = dictlookup(getlang(LangHANJA), &key, im.kouho, Maxkouho); selectfirst(); } static void searchquery(void) { if(search.lang == LangEMOJI) emojiquery(); else if(search.lang == LangHANJA) hanjaquery(); } static int searchlang(Rune c, u32int mod) { if(mod & (Malt|Msuper)) return 0; if(c == LangEMOJI || c == LangHANJA) return c; return 0; } static void endsearch(void) { search.lang = 0; sclear(&search.raw); sclear(&search.text); clearkouho(); } static void startsearch(int lang, Str *com) { if(haspre(&im)) commit(com); sclear(&im.pre); sclear(&im.raw); clearkouho(); search.lang = lang; sclear(&search.raw); sclear(&search.text); } static void commitsearch(Str *com) { sappend(com, &search.text); endsearch(); } static int picksearch(int n, Str *com) { if(n < 0 || n >= im.nkouho) return 0; sappend(com, &im.kouho[n]); endsearch(); return 1; } static int searchkey(u32int ks, u32int mod, Str *com) { int n; Rune c; if(ismodkey(ks)) return 0; n = movedelta(ks); if(n != 0){ if(im.nkouho == 0) return 1; movekouho(n); return 1; } n = numberedkouho(ks, mod); if(n >= 0){ picksearch(n, com); return 1; } if(ks == Ktab){ if(im.nkouho == 0) return 1; if(mod & Mshift){ if(im.sel <= 0) im.sel = im.nkouho - 1; else im.sel--; }else if(im.sel >= im.nkouho - 1) im.sel = 0; else im.sel++; return 1; } if(ks == Kret){ if(im.nkouho > 0) return picksearch(im.sel >= 0 ? im.sel : 0, com); commitsearch(com); return 1; } if(ks == Kback){ if(search.raw.n == 0){ endsearch(); return 0; } spopr(&search.raw); searchquery(); return 1; } if(ks == Kesc){ endsearch(); return 1; } if(ks >= Kspec || (ks < ' ' && !(mod & Mctrl)) || (mod & (Malt|Msuper))){ commitsearch(com); return 0; } if(mod & Mctrl){ c = ctrlkey(ks); n = searchlang(c, mod); if(n != 0){ if(n == search.lang) endsearch(); else startsearch(n, com); return 1; } endsearch(); if(setlang(c)) return 1; return 0; } if(search.raw.n >= Maxrunes){ commitsearch(com); return 0; } if(im.l->lang == LangKO) ks = kokey(ks, mod); sputr(&search.raw, ks); searchquery(); return 1; } static int transition(u32int ks, u32int mod, Str *com) { int n; Rune c; if(ismodkey(ks)) return 0; if(search.lang) return searchkey(ks, mod, com); n = movedelta(ks); if(n != 0){ if(im.nkouho == 0) return 0; movekouho(n); return 1; } n = numberedkouho(ks, mod); if(n >= 0){ sappend(com, &im.kouho[n]); reset(); return 1; } if(ks == Ktab || ks == Kret){ if(candidatechosen && im.sel >= 0 && im.sel < im.nkouho){ sappend(com, &im.kouho[im.sel]); reset(); return 1; } if(haspre(&im)){ commit(com); reset(); return 1; } return 0; } if(ks == Kback){ if(!haspre(&im)) return 0; im.l->back(&im); if(!haspre(&im)){ reset(); return 1; } if(isjp(&im)) dictqjp(); else clearkouho(); return 1; } if(ks == Kesc){ if(!haspre(&im)) return 0; reset(); return 1; } if(ks >= Kspec || (mod & (Malt|Msuper))){ commit(com); reset(); return 0; } if(mod & Mctrl){ c = ctrlkey(ks); n = searchlang(c, mod); if(n != 0){ startsearch(n, com); return 1; } if(getlang(c) != nil){ if(haspre(&im)) commit(com); reset(); setlang(c); return 1; } if(haspre(&im)) commit(com); reset(); return 0; } if(ks == '0' && im.nkouho > 0){ commit(com); reset(); return 1; } if(ks > 0x7f || ks == ' '){ if(!haspre(&im)) return 0; commit(com); reset(); if(com->n >= Maxrunes) return 0; sputr(com, ks); return 1; } if(im.l->trans == nil) return 0; if(im.pre.n >= Maxrunes || (isjp(&im) && im.pre.n + im.raw.n >= Maxrunes)){ commit(com); reset(); return 0; } if(im.l->lang == LangKO) ks = kokey(ks, mod); n = dotrans(ks, com); return n; } static int samestate(Im *a, Search *as, Im *b, Search *bs) { int i; if(a->l != b->l || scmp(&a->pre, &b->pre) != 0 || scmp(&a->raw, &b->raw) != 0 || a->nkouho != b->nkouho || a->sel != b->sel || as->lang != bs->lang || scmp(&as->raw, &bs->raw) != 0 || scmp(&as->text, &bs->text) != 0) return 0; for(i = 0; i < a->nkouho; i++) if(scmp(&a->kouho[i], &b->kouho[i]) != 0) return 0; return 1; } static void savestate(Im *p, Search *s) { int i; memset(p, 0, sizeof *p); memset(s, 0, sizeof *s); p->l = im.l; sappend(&p->pre, &im.pre); sappend(&p->raw, &im.raw); p->nkouho = im.nkouho; p->sel = im.sel; for(i = 0; i < im.nkouho; i++) sappend(&p->kouho[i], &im.kouho[i]); s->lang = search.lang; sappend(&s->raw, &search.raw); sappend(&s->text, &search.text); } static int keystrokeforce(u32int ks, u32int mod, Str *com, int force) { Im oldim; Search oldsearch; int eaten, ncom; savestate(&oldim, &oldsearch); ncom = com->n; eaten = transition(ks, mod, com); if(force || eaten || com->n != ncom || !samestate(&oldim, &oldsearch, &im, &search)) redraw(); return eaten; } static int keystroke(u32int ks, u32int mod, Str *com) { return keystrokeforce(ks, mod, com, 0); } static void init(void) { memset(&im, 0, sizeof(im)); im.l = getlang(LangEN); im.sel = -1; memset(&search, 0, sizeof search); memset(&caret, 0, sizeof caret); activeowner = nil; activecap = 0; candidatechosen = 0; visible = 0; } static int meaningful(Keyreq *kr) { return kr->ks != 0 && !ismodkey(kr->ks); } static int setactivecap(int cap) { int old; old = activecap & Cclientpreedit; activecap &= ~Cclientpreedit; activecap |= cap & Cclientpreedit; return old != (activecap & Cclientpreedit); } static int careteq(Caret *a, Caret *b) { return a->valid == b->valid && a->x == b->x && a->y == b->y && a->h == b->h; } static void imhandlekey(Keyreq *kr) { Keyres res; void *oldowner; int changed; oldowner = activeowner; sclear(&res.commit); sclear(&res.preedit); res.eaten = 1; switch(kr->op){ case Keyrelease: if(kr->owner == activeowner){ reset(); activeowner = nil; activecap = 0; redraw(); } break; case Keyreset: if(kr->owner == activeowner){ reset(); redraw(); } break; case Keycaret: if(kr->owner == activeowner && !careteq(&caret, &kr->caret)){ caret = kr->caret; if(visible) redraw(); } break; case Keycap: res.eaten = 0; if(activeowner != nil && kr->owner == activeowner){ res.eaten = 1; if(setactivecap(kr->cap)) redraw(); } break; case Keypress: res.eaten = 0; changed = 0; if(activeowner != nil && kr->owner == activeowner && setactivecap(kr->cap)) changed = 1; if(meaningful(kr)){ if(kr->owner != activeowner){ reset(); activeowner = kr->owner; activecap = kr->cap & Cclientpreedit; caret = kr->caret; changed = 1; }else if(kr->caret.valid && !careteq(&caret, &kr->caret)){ caret = kr->caret; changed = 1; } if(changed) res.eaten = keystrokeforce(kr->ks, kr->mod, &res.commit, 1); else res.eaten = keystroke(kr->ks, kr->mod, &res.commit); }else if(changed) redraw(); break; } if(activeowner != oldowner) ximownernotify(); if(kr->owner == activeowner){ if(search.lang) res.preedit = search.text; else impre(&res.preedit); } chansend(kr->reply, &res); } void imthread(void*) { Keyreq kr; threadsetname("im"); init(); for(;;){ chanrecv(keyc, &kr); imhandlekey(&kr); } } int mapget(Trie *t, Str *key, Str *out) { char *v; int vlen; sclear(out); if(key->n == 0 || trielookup(t, key, &v, &vlen) != TrieExact) return 0; return sinit(out, v, vlen); }