Files
strans/strans.c
Hojun-Cho ebcec3af6b fix(engine): send clients the composed Telex text, not the keys
For Vietnamese the preedit is the map key ('as', 'oong'); only the popup
and the commit path looked it up. Clients with inline preedit therefore
showed 'as' while Enter committed 'á'. impre() now maps once for every
reader, and snapshot() no longer maps a second time.
2026-08-16 15:57:55 +09:00

1056 lines
17 KiB
C

#include "dat.h"
#include "fn.h"
static Im im;
static int visible = 0;
static void *activeowner;
static int activecap;
static Caret caret;
static u32int dictseq;
static int candidatechosen;
static void dictqmap(Im*);
static void dictqjp(Im*);
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, nil},
{LangJP, "hira", "kanji", transmap, backjp, dictqjp, nil, nil},
{LangJPK, "kata", nil, transmap, backjp, dictqjp, nil, nil},
{LangKO, nil, nil, transko, backko, nil, nil, nil},
{LangHANJA, nil, "hanja", nil, nil, nil, nil, nil},
{LangEMOJI, nil, "emoji", nil, nil, nil, nil, nil},
{LangVI, "telex", nil, transvi, backvi, dictqmap, nil, nil},
};
int nlang = nelem(langs);
static void
clearkouho(void)
{
dictseq++;
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).
*/
static int
jpreading(Im *p, Str *s)
{
Str mapped;
*s = p->pre;
if(p->raw.n == 0)
return 1;
if(mapget(p->l->map, &p->raw, &mapped)){
sappend(s, &mapped);
return 1;
}
sappend(s, &p->raw);
return 0;
}
static int
haspre(Im *p)
{
return p->pre.n != 0 || (isjp(p) && 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, s);
else if(!mapget(im.l->map, &im.pre, s))
*s = im.pre;
}
static void
setkouho(Dictres *res)
{
int i;
clearkouho();
for(i = 0; i < res->nkouho; i++){
im.kouho[i] = res->kouho[i];
im.nkouho++;
}
selectfirst();
}
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();
}
static void
dictsend(Im *im, Str *key)
{
Dictreq old, req;
req.key = *key;
req.lang = im->l->lang;
req.seq = dictseq;
impre(&req.pre);
/* imthread is the sole producer; discard every obsolete queued lookup. */
while(channbrecv(dictreqc, &old) > 0)
;
channbsend(dictreqc, &req);
}
static void
dictqmap(Im *im)
{
Str dict;
clearkouho();
if(im->l->map == nil)
return;
if(!mapget(im->l->map, &im->pre, &dict))
return;
dictsend(im, &dict);
}
static void
dictqjp(Im *im)
{
Str reading;
clearkouho();
if(im->l->dict == nil || !jpreading(im, &reading) || reading.n == 0)
return;
dictsend(im, &reading);
}
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, &val);
sappend(com, &val);
}else if(p->l->map != nil && mapget(p->l->map, &p->pre, &val))
sappend(com, &val);
else
sappend(com, &p->pre);
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 int
transjp(Im *p, Rune c, Str *com)
{
Str key, mapped;
if(c == '\'' && p->raw.n == 1 && p->raw.r[0] == 'n'){
if(mapget(p->l->map, &p->raw, &mapped))
sappend(&p->pre, &mapped);
sclear(&p->raw);
return 1;
}
key = p->raw;
if(key.n < Maxrunes)
sputr(&key, c);
sclear(&mapped);
if(maplookup(p->l->map, &key, &mapped)){
p->raw = key;
if(issmalltsu(&mapped)){
sappend(&p->pre, &mapped);
keeptail(&p->raw);
}
return 1;
}
if(p->raw.n != 0 && mapget(p->l->map, &p->raw, &mapped)){
sappend(&p->pre, &mapped);
sclear(&p->raw);
sclear(&key);
sputr(&key, c);
if(maplookup(p->l->map, &key, &mapped)){
p->raw = key;
return 1;
}
}
commitim(p, com);
return 0;
}
static int
dotrans(Rune c, Str *com)
{
Emit e;
Str mapped;
if(isjp(&im)){
e.eat = transjp(&im, c, com);
if(e.eat)
dictqjp(&im);
else
clearkouho();
return e.eat;
}
e = im.l->trans(&im, c);
if(im.l->lang == LangVI){
if(e.s.n > 0 || !e.eat)
sclear(&im.raw);
if(e.eat && e.next.n > 0){
if(im.raw.n >= Maxrunes){
if(!mapget(im.l->map, &e.next, &mapped))
mapped = e.next;
sappend(com, &mapped);
reset();
return e.eat;
}
sputr(&im.raw, c);
}
}
clearkouho();
if(e.s.n > 0)
sappend(com, &e.s);
sclear(&im.pre);
sappend(&im.pre, &e.next);
if(e.eat && e.dict.n > 0){
dictsend(&im, &e.dict);
}
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;
Trie *t;
t = im->l->map;
key = im->pre;
sputr(&key, c);
if(maplookup(t, &key, &e.dict)){
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, &e.dict))
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';
}
static void
transstr(Lang *l, Str *raw, Str *out)
{
Im q;
Emit e;
Str mapped;
int i;
sclear(out);
if(l == nil || l->trans == nil || (l->trans == transmap && l->map == nil)){
*out = *raw;
return;
}
memset(&q, 0, sizeof q);
q.l = l;
if(isjp(&q)){
for(i = 0; i < raw->n; i++)
if(!transjp(&q, raw->r[i], out) && out->n < Maxrunes)
sputr(out, raw->r[i]);
if(haspre(&q))
commitim(&q, out);
return;
}
for(i = 0; i < raw->n; i++){
e = l->trans(&q, raw->r[i]);
sappend(out, &e.s);
q.pre = e.next;
if(!e.eat && out->n < Maxrunes)
sputr(out, raw->r[i]);
}
if(q.pre.n == 0)
return;
if(l->map != nil && mapget(l->map, &q.pre, &mapped))
sappend(out, &mapped);
else
sappend(out, &q.pre);
}
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 void
searchlookup(int lang, Str *key, Dictres *res)
{
Dictreq req;
req.key = *key;
req.pre = req.key;
req.lang = lang;
req.seq = 0;
dictlookup(&req, res);
}
static void
emojiquery(void)
{
static Dictres res;
Str raw, local;
int i, rawhit, localhit;
raw = search.raw;
foldascii(&raw);
transstr(im.l, &search.raw, &local);
foldascii(&local);
clearkouho();
searchlookup(LangEMOJI, &raw, &res);
rawhit = res.nkouho != 0;
for(i = 0; i < res.nkouho; i++)
addkouho(&res.kouho[i]);
localhit = 0;
if(scmp(&raw, &local) != 0){
searchlookup(LangEMOJI, &local, &res);
localhit = res.nkouho != 0;
for(i = 0; i < res.nkouho; i++)
addkouho(&res.kouho[i]);
}
search.text = rawhit || !localhit ? raw : local;
selectfirst();
}
static void
hanjaquery(void)
{
static Dictres res;
Str key;
transstr(im.l, &search.raw, &key);
search.text = key;
if(key.n == 1 && key.r[0] >= 0xac00 && key.r[0] <= 0xd7a3){
searchlookup(LangHANJA, &key, &res);
setkouho(&res);
}else
clearkouho();
}
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(im.l->dictq != nil)
im.l->dictq(&im);
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;
dictseq = 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);
}
static void
dictresult(Dictres *res)
{
Str pre;
impre(&pre);
if(search.lang || res->seq != dictseq || res->lang != im.l->lang ||
scmp(&res->key, &pre) != 0)
return;
setkouho(res);
redraw();
}
void
imthread(void*)
{
static Dictres res;
Keyreq kr;
Alt alts[] = {
{keyc, &kr, CHANRCV, nil},
{dictresc, &res, CHANRCV, nil},
{nil, nil, CHANEND, nil},
};
threadsetname("im");
init();
for(;;){
switch(alt(alts)){
case 0:
imhandlekey(&kr);
break;
case 1:
dictresult(&res);
break;
}
}
}
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);
}