Files
strans/strans.c
Hojun-Cho a557fb114a engine: one trans contract for every language
Japanese was never dispatched through the Lang table: dotrans and
transstr both special-cased it into transjp, which edited Im in place
and wrote commits through a side argument, while transko popped and
cleared a pre that every caller overwrote anyway, and Vietnamese kept
its key history in dotrans. Emit now carries the new raw state next to
the new pre; transjp, transko and transvi are pure functions of (Im,
key), and dotrans and transstr have a single path. Vietnamese history
bookkeeping lives in vi.c, where the full-history flush no longer
resets the caret as a side effect; istone was toneidx() >= 0.
2026-08-16 16:04:57 +09:00

945 lines
16 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 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);
}