Files
strans/strans.c
Hojun-Cho 5faefd9a87 dict: lookups take a trie; a prefix search is its own function; no self-key rule
dictlookup took a Lang for two reasons that were not its business: to
know whether to walk below the key (the emoji dictionary) and to drop a
candidate equal to the key except there.  No dictionary lists its key
among its candidates — the rule was left from a first version that put
the reading first itself — so it goes, and the walk is dictprefix(),
called by the emoji search alone.
2026-08-17 01:31:51 +09:00

960 lines
18 KiB
C

#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->dict, &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 Japanese reading; none is chosen yet. */
static void
dictqjp(void)
{
Str reading;
clearkouho();
if(!isjp(&im) || !jpreading(im.l->map, &im.pre, &im.raw, &reading) ||
reading.n == 0)
return;
im.nkouho = dictlookup(im.l->dict, &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;
impre(p, &val);
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;
dictqjp();
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;
}
/* ASCII only: what the maps and dictionaries fold too. */
static Rune
lower(Rune c)
{
if(c >= 'A' && c <= 'Z')
c += 'a' - 'A';
return c;
}
static Rune
ctrlkey(Rune c)
{
c = lower(c);
if(c >= 'a' && c <= 'z')
return c - 'a' + 1;
return c;
}
/* Caps Lock does not shift a Korean key; Shift does. */
static Rune
kokey(Rune c, u32int mod)
{
c = lower(c);
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++)
s->r[i] = lower(s->r[i]);
}
/*
* 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 = dictprefix(getlang(LangEMOJI)->dict, &raw, kouho, Maxkouho);
rawhit = n != 0;
for(i = 0; i < n; i++)
addkouho(&kouho[i]);
localhit = 0;
if(scmp(&raw, &local) != 0){
n = dictprefix(getlang(LangEMOJI)->dict, &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)->dict, &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;
}
/*
* A chord with Alt or Super, or Ctrl with Shift, is the application's
* (Ctrl+Shift+V pastes); only a plain Ctrl+letter is a strans command.
*/
static int
chord(u32int mod)
{
return (mod & ~Mshift) != 0 && mod != Mctrl;
}
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)) || chord(mod)){
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(im.l->lang == LangKO)
ks = kokey(ks, mod);
else if(isjp(&im))
ks = lower(ks); /* romaji is case-blind, Caps Lock included */
/* The Korean and Japanese keyboards' own keys, as the Ctrl chords
* and Space they stand for. */
switch(ks){
case Khangul: ks = im.l->lang == LangKO ? 't' : 's'; mod = Mctrl; break;
case Khanja: ks = 'h'; mod = Mctrl; break;
case Kzenkaku: ks = isjp(&im) ? 't' : 'n'; mod = Mctrl; break;
case Kkana: ks = im.l->lang == LangJP ? 'k' : 'n'; mod = Mctrl; break;
case Khenkan:
if(isjp(&im)){
ks = ' ';
mod &= Mshift;
}else{
ks = 'n';
mod = Mctrl;
}
break;
case Kmuhenkan: ks = 't'; mod = Mctrl; break;
}
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))
dictqjp();
else
reset();
return 1;
}
if(ks == Kesc){
if(!haspre(&im))
return 0;
if(im.sel >= 0)
im.sel = -1;
else
reset();
return 1;
}
if(ks >= Kspec || chord(mod)){
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(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 = kr->owner == activeowner;
if(res.eaten)
activecap = kr->cap & Cclientpreedit;
break;
case Keypress:
res.eaten = 0;
if(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;
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);
}
}