#include "dat.h" #include "fn.h" static Im im; static void *activeowner; static int activecap; static Caret caret; static Drawcmd lastdraw; static Emit transjp(Im*, Rune); static void backjp(Im*); /* seed is what Ctrl+H found pending; raw the keys typed since; text both. */ typedef struct Search Search; struct Search { int lang; Str seed; 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; } /* sel is the chosen candidate; -1 while the user has not moved to one. */ static void clearkouho(void) { im.nkouho = 0; im.sel = -1; } 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; } /* Space and Tab step through the candidates and wrap around. */ static void cyclekouho(int delta) { if(im.sel < 0) im.sel = delta > 0 ? 0 : im.nkouho - 1; else im.sel = (im.sel + delta + im.nkouho) % im.nkouho; } 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; } 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); } /* SKK's letter for the okurigana a kana begins: its row, or its vowel. */ static Rune okuriletter(Rune r) { static char *rows[] = { "aあ", "iい", "uう", "eえ", "oお", "kかきくけこ", "gがぎぐげご", "sさしすせそ", "zざじずぜぞ", "tたちつてとっ", "dだぢづでど", "nなにぬねのん", "hはひふへほ", "bばびぶべぼ", "pぱぴぷぺぽ", "mまみむめも", "yやゆよ", "rらりるれろ", "wわ", }; int i; for(i = 0; i < nelem(rows); i++) if(utfrune(rows[i]+1, r) != nil) return rows[i][0]; return 0; } /* * SKK keys a verb or adjective by its stem and the letter of the * okurigana that follows (かk: 書, 描, ...). Every split of the reading * is tried, longest stem first, and the okurigana is put back; a っ * ending the stem (いっt: 言, 行) is written in kana too. */ static void dictqokuri(Str *reading) { Str key, okuri, kouho[Maxkouho]; Rune c; int i, j, n; for(i = reading->n - 1; i > 0; i--){ c = okuriletter(reading->r[i]); if(c == 0) continue; key = *reading; key.n = i; sputr(&key, c); n = dictlookup(im.l, &key, kouho, Maxkouho); sclear(&okuri); for(j = i - (reading->r[i-1] == L'っ'); j < reading->n; j++) sputr(&okuri, reading->r[j]); for(j = 0; j < n; j++){ sappend(&kouho[j], &okuri); addkouho(&kouho[j]); } } } /* Kanji candidates for a complete reading; none is chosen yet. */ 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); dictqokuri(&reading); } /* The reading in Katakana is the last candidate, as in every IME. */ static void katakouho(void) { Str reading, k; int i; if(!jpreading(im.l->map, &im.pre, &im.raw, &reading)) return; k = reading; for(i = 0; i < k.n; i++) if(k.r[i] >= L'ぁ' && k.r[i] <= L'ゖ') k.r[i] += L'ァ' - L'ぁ'; if(scmp(&k, &reading) != 0) addkouho(&k); } 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 backjp(Im *p) { if(p->raw.n != 0) spopr(&p->raw); else spopr(&p->pre); } /* * kk and the like are a sokuon: っ, and the consonant stays pending, as * ch does after tch. xtu and ltu are っ itself. */ static int issokuon(Str *key, Str *mapped) { return mapped->n == 1 && (mapped->r[0] == L'っ' || mapped->r[0] == L'ッ') && (key->r[0] == key->r[1] || (key->r[0] == 't' && key->r[1] == 'c')); } 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; key = e.raw; sputr(&key, c); if(mapmatch(p->l->map, &key, &mapped) != TrieMiss){ e.raw = key; if(issokuon(&key, &mapped)){ sappend(&e.next, &mapped); keeptail(&e.raw); } return e; } /* The pending romaji is done: kana if it is complete, else the * letters as typed, for Backspace to mend. */ if(!mapget(p->l->map, &e.raw, &mapped)) mapped = e.raw; 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. */ e.s = e.next; sclear(&e.next); 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, * after pre, which may be nil, was already pending. */ void transstr(Lang *l, Str *pre, Str *raw, Str *out) { Im q; Emit e; int i; memset(&q, 0, sizeof q); q.l = l; if(pre != nil) q.pre = *pre; sclear(out); if(l->trans == nil){ sappend(out, &q.pre); sappend(out, raw); return; } 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); } /* 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, nil, &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(); } /* The keys typed since Ctrl+H go on composing from the seed. */ static void hanjaquery(void) { transstr(im.l, &search.seed, &search.raw, &search.text); clearkouho(); im.nkouho = dictlookup(getlang(LangHANJA), &search.text, 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) { memset(&search, 0, sizeof search); clearkouho(); } static void reset(void) { sclear(&im.pre); sclear(&im.raw); endsearch(); } /* * Whatever is pending becomes committed text: the search query, a chosen * candidate, else the reading. Enter does this, and so do a reset and a * lost focus. */ static void flush(Str *com) { if(search.lang) sappend(com, &search.text); else if(im.sel >= 0) sappend(com, &im.kouho[im.sel]); else commitim(&im, com); reset(); } /* * A search takes over the keys. Hanja converts the pending syllable, so * it becomes the query; anything else pending is committed. */ static void startsearch(int lang, Str *com) { Str seed; sclear(&seed); if(lang == LangHANJA && !search.lang && im.sel < 0) impre(&im, &seed); else flush(com); reset(); search.lang = lang; search.seed = seed; if(lang == LangHANJA) searchquery(); } 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) cyclekouho(mod & Mshift ? -1 : 1); return 1; } if(ks == Kret){ if(im.nkouho > 0) picksearch(max(im.sel, 0), com); else flush(com); return 1; } if(ks == Kback){ if(search.raw.n != 0) spopr(&search.raw); else if(search.seed.n != 0) spopr(&search.seed); else{ endsearch(); return 1; } searchquery(); return 1; } if(ks == Kesc){ im.pre = search.seed; /* the syllable Ctrl+H took is pending again */ endsearch(); return 1; } if(ks >= Kspec || (ks < ' ' && !(mod & Mctrl)) || (mod & (Malt|Msuper))){ flush(com); return 0; } if(mod & Mctrl){ c = ctrlkey(ks); n = searchlang(c); if(n != 0 && n != search.lang){ startsearch(n, com); return 1; } flush(com); return n != 0 || setlang(c); } if(search.raw.n >= Maxrunes){ flush(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); else if(isjp(&im) && ks >= 'A' && ks <= 'Z') ks += 'a' - 'A'; /* romaji is case-blind, Caps Lock included */ /* The Korean keyboard's 한/영 and 한자 keys are Ctrl+S or Ctrl+T, and Ctrl+H. */ if(ks == Khangul){ ks = im.l->lang == LangKO ? 't' : 's'; mod = Mctrl; }else if(ks == Khanja){ ks = 'h'; mod = Mctrl; } 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 == ' ' && isjp(&im) && haspre(&im) && !(mod & ~Mshift)){ /* Space converts: it steps through the candidates, Katakana * last; a reading with none is committed. */ katakouho(); if(im.nkouho != 0) cyclekouho(mod & Mshift ? -1 : 1); else flush(com); return 1; } if(ks == Ktab || ks == Kret || (ks == Kesc && !isjp(&im))){ 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; if(im.sel >= 0){ im.sel = -1; /* back from the candidate to the reading */ return 1; } 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; if(im.sel >= 0) im.sel = -1; else reset(); return 1; } if(ks >= Kspec || (mod & (Malt|Msuper))){ flush(com); return 0; } if(mod & Mctrl){ c = ctrlkey(ks); n = searchlang(c); if(n != 0){ startsearch(n, com); return 1; } flush(com); return setlang(c); } if(ks == '0' && isjp(&im) && haspre(&im)){ im.sel = -1; /* 0 commits the reading, as 1-9 pick candidates */ flush(com); return 1; } if(ks > 0x7f || ks == ' '){ if(!haspre(&im)) return 0; flush(com); if(com->n >= Maxrunes) return 0; sputr(com, ks); return 1; } if(im.l->trans == nil) return 0; if(im.sel >= 0) flush(com); /* typing on takes the chosen candidate */ 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 commitim(&im, 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; 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); } }