#include "dat.h" #include "fn.h" static Im im; static void *activeowner; static int activecap; static Caret caret; static int candidatechosen; static Drawcmd lastdraw; static Emit transjp(Im*, Rune); static void backjp(Im*); 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); /* Trie match kind for key; on an exact match out holds the value. */ static int mapmatch(Trie *t, Str *key, Str *out) { char *v; int match, vlen; sclear(out); if(key->n == 0) return TrieMiss; match = trielookup(t, key, &v, &vlen); if(match == TrieExact && !sinit(out, v, vlen)) return TrieMiss; return match; } int mapget(Trie *t, Str *key, Str *out) { return mapmatch(t, key, out) == TrieExact; } 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) { switch(ks){ case Kup: return -1; case Kdown: return 1; case Kpgup: return -Maxdisp; case Kpgdown: return Maxdisp; } 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. */ void impre(Im *p, Str *s) { if(isjp(p)) jpreading(p->l->map, &p->pre, &p->raw, s); else if(!mapget(p->l->map, &p->pre, s)) *s = p->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(&im, &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 int careteq(Caret *a, Caret *b) { return a->valid == b->valid && a->x == b->x && a->y == b->y && a->h == b->h; } static int shown(Drawcmd *dc) { return dc->pre.n != 0 || dc->nkouho != 0; } static int samedraw(Drawcmd *a, Drawcmd *b) { int i; if(!shown(a) && !shown(b)) return 1; if(a->nkouho != b->nkouho || a->sel != b->sel || a->first != b->first || a->total != b->total || scmp(&a->pre, &b->pre) != 0 || !careteq(&a->caret, &b->caret)) return 0; for(i = 0; i < a->nkouho; i++) if(scmp(&a->kouho[i], &b->kouho[i]) != 0) return 0; return 1; } /* Sends the popup a new picture only when it would look different. */ static void redraw(void) { Drawcmd dc, junk; snapshot(&dc); if(samedraw(&dc, &lastdraw)) return; lastdraw = dc; /* imthread is the sole producer; keep only the newest pending draw. */ while(channbrecv(drawc, &junk) > 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 *p) { if(p->raw.n != 0) spopr(&p->raw); else spopr(&p->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(mapmatch(p->l->map, &key, &mapped) != TrieMiss){ 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(mapmatch(p->l->map, &key, &mapped) != TrieMiss){ 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; } /* * The pending text is the key sequence itself while it is a prefix of some * map entry; the map value is what gets shown and committed. */ Emit transmap(Im *p, Rune c) { Emit e = {0}; Str key, mapped; Trie *t; t = p->l->map; key = p->pre; sputr(&key, c); if(mapmatch(t, &key, &mapped) != TrieMiss){ e.eat = 1; e.next = key; return e; } if(!mapget(t, &p->pre, &e.s)) e.s = p->pre; sclear(&key); sputr(&key, c); if(mapmatch(t, &key, &mapped) == TrieMiss) 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; } /* Modifier presses and unmapped keys never engage the engine. */ int keymeaningful(u32int ks) { return ks != 0 && !ismodkey(ks); } 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. */ 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) { return c == LangEMOJI || c == LangHANJA ? c : 0; } static void endsearch(void) { search.lang = 0; sclear(&search.raw); sclear(&search.text); clearkouho(); } static void commitsearch(Str *com) { sappend(com, &search.text); endsearch(); } /* A search commits whatever is pending and takes over the keys. */ static void startsearch(int lang, Str *com) { commit(com); commitsearch(com); search.lang = lang; } /* * Whatever is pending becomes committed text, a moved-to candidate * first: Enter does this, and so do a reset and a lost focus. */ static void flush(Str *com) { if(search.lang) commitsearch(com); else if(candidatechosen && im.sel >= 0 && im.sel < im.nkouho) sappend(com, &im.kouho[im.sel]); else commit(com); reset(); } static void picksearch(int n, Str *com) { sappend(com, &im.kouho[n]); endsearch(); } static int searchkey(u32int ks, u32int mod, Str *com) { int n; Rune c; 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) picksearch(max(im.sel, 0), com); else 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); if(n != 0 && n != search.lang){ startsearch(n, com); return 1; } commitsearch(com); return n != 0 || setlang(c); } if(search.raw.n >= Maxrunes){ commitsearch(com); return 0; } 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(im.l->lang == LangKO) ks = kokey(ks, mod); if(search.lang) return searchkey(ks, mod, com); n = movedelta(ks); if(n != 0 && im.nkouho != 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(!haspre(&im)) return 0; flush(com); /* Japanese confirms with the key; Korean and Telex let it on. */ return isjp(&im); } 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); if(n != 0){ startsearch(n, com); return 1; } commit(com); reset(); return setlang(c); } 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)){ /* A full reading commits; a pending romaji syllable stays. */ if(isjp(&im)) sappend(com, &im.pre); else commit(com); sclear(&im.pre); clearkouho(); } return dotrans(ks, com); } 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; memset(&lastdraw, 0, sizeof lastdraw); } /* * The engine belongs to whichever context last typed a real key; only * the owner's requests change state. A reset or release hands the * owner its pending text back as commit. Every request ends in * redraw(), which sends the popup a picture only if something visible * changed. */ static void imhandlekey(Keyreq *kr) { Keyres res; sclear(&res.commit); sclear(&res.preedit); res.eaten = 1; switch(kr->op){ case Keyrelease: if(kr->owner == activeowner){ flush(&res.commit); activeowner = nil; activecap = 0; } break; case Keyreset: if(kr->owner == activeowner) flush(&res.commit); break; case Keycaret: if(kr->owner == activeowner) caret = kr->caret; break; case Keycap: res.eaten = activeowner != nil && kr->owner == activeowner; if(res.eaten) activecap = kr->cap & Cclientpreedit; break; case Keypress: res.eaten = 0; if(activeowner != nil && kr->owner == activeowner) activecap = kr->cap & Cclientpreedit; if(keymeaningful(kr->ks)){ if(kr->owner != activeowner){ reset(); activeowner = kr->owner; activecap = kr->cap & Cclientpreedit; caret = kr->caret; }else if(kr->caret.valid) caret = kr->caret; res.eaten = transition(kr->ks, kr->mod, &res.commit); } break; } redraw(); if(kr->owner == activeowner){ if(search.lang) res.preedit = search.text; else impre(&im, &res.preedit); } chansend(kr->reply, &res); } void imthread(void*) { Keyreq kr; threadsetname("im"); init(); for(;;){ chanrecv(keyc, &kr); imhandlekey(&kr); } }