Files
strans/strans.c
Hojun-Cho 0f77e0e9ac engine: the chosen candidate is what the client shows
The preedit sent back to the owner was always impre(), the reading, so
かく stayed underlined in the application while the popup showed 書く and
Enter inserted 書く.  Nothing was hidden -- the popup had it -- but the
text did not stand where it was going to land, and mozc, fcitx5 and kime
all put the candidate inline.

Only the reply changes.  impre() still answers the reading everywhere it
is the reading that is wanted: commitim, the okurigana mark, the seed
Ctrl+H takes, and snapshot, so a popup that draws the preedit itself
keeps the reading above the list where it says something the highlighted
row does not.  Consulting im.sel here is safe where consulting it inside
impre() would not be: transstr memsets a local Im, whose sel is 0 and not
-1, and this reply only ever reads the engine's own.

Nothing in the suite pinned the old answer, so the test is new.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:25:55 +09:00

1122 lines
23 KiB
C

#include "dat.h"
#include "fn.h"
static Im im;
static void *activeowner;
static void *lostowner; /* the context the engine was taken from */
static Str lost;
static int clientpre; /* the owner draws its own preedit */
static Caret caret;
static Str surround; /* the owner's own text just before the cursor */
static int takeback; /* runes of it a pick takes back before committing */
static Rune modemark; /* the mode the last key switched to */
static int okuriat = -1; /* where Shift marked the okurigana, or -1 */
static Drawcmd lastdraw;
static Emit transjp(Im*, Rune);
static void backjp(Im*);
/*
* back is the client's own text the reading reached into; seed what
* Ctrl+H found pending; raw the keys typed since; text all three.
*/
typedef struct Search Search;
struct Search
{
int lang;
Str back;
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;
}
/* Nothing chosen yet: Down takes the first candidate, PageDown pages. */
static void
movekouho(int delta)
{
if(im.sel < 0)
im.sel = delta == Maxdisp ? Maxdisp : 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, and any romaji that never became kana,
* in pre, and the romaji still being typed 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;
}
/* The query as it is still ours to show: the client has the rest. */
static void
searchpre(Str *s)
{
int i;
sclear(s);
for(i = search.back.n; i < search.text.n; i++)
sputr(s, search.text.r[i]);
}
static void
snapshot(Drawcmd *dc)
{
Str pre;
int i, first, n;
memset(dc, 0, sizeof *dc);
if(search.lang)
searchpre(&pre);
else
impre(&im, &pre);
if(!clientpre)
dc->pre = pre;
if(search.lang && pre.n == 0) /* a search just begun shows itself */
sputr(&dc->pre, search.lang == LangEMOJI ? L'' : L'');
else if(modemark != 0 && pre.n == 0 && im.nkouho == 0)
sputr(&dc->pre, modemark);
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ざずぜぞ",
"jじ",
"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: 書, 描, ...). The split the user marked
* is tried alone; otherwise every split is, longest stem first. The
* okurigana is put back, and a っ ending the stem (いっt: 言, 行) is
* written in kana too.
*/
static void
dictqokuri(Str *reading, int split)
{
Str key, okuri, kouho[Maxkouho];
Rune c;
int i, j, n;
for(i = split >= 0 ? split : reading->n - 1; i > 0; i--){
c = okuriletter(reading->r[i]);
if(c == 0)
break;
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]);
}
if(split >= 0)
break;
}
}
/* Kanji candidates for a complete Japanese reading; none is chosen yet. */
static void
dictqjp(void)
{
Str reading, exact[Maxkouho];
int i, n;
clearkouho();
if(!isjp(&im) || !jpreading(im.l->map, &im.pre, &im.raw, &reading) ||
reading.n == 0)
return;
/* What Shift marked comes first; the dictionary's own readings next. */
if(okuriat > 0 && okuriat < reading.n)
dictqokuri(&reading, okuriat);
n = dictlookup(im.l->dict, &reading, exact, Maxkouho);
for(i = 0; i < n; i++)
addkouho(&exact[i]);
dictqokuri(&reading, -1);
}
/* 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);
}
/* What the popup shows for a moment when the mode changes. */
static Rune
langmark(int lang)
{
switch(lang){
case LangJP: return L'';
case LangJPK: return L'';
case LangKO: return L'';
case LangVI: return L'ă';
}
return L'A';
}
static int
setlang(int c)
{
Lang *l;
l = getlang(c);
if(l == nil)
return 0;
if(l != im.l)
modemark = langmark(l->lang);
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);
}
/* Deletes the last kana as shown, not the last key: kya loses ゃ. */
static void
backjp(Im *p)
{
Str v;
if(p->raw.n == 0)
spopr(&p->pre);
else if(mapget(p->l->map, &p->raw, &v)){
spopr(&v);
sappend(&p->pre, &v);
sclear(&p->raw);
}else
spopr(&p->raw);
}
/*
* 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;
}
/* A modifier press or an unmapped key is key 0: it engages nothing. */
int
keymeaningful(u32int ks)
{
return ks != 0;
}
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, folded
* as the dictionary is; the query shown is the keys while they match
* something, else the transliteration, as typed.
*/
static void
emojiquery(void)
{
Str kouho[Maxkouho], raw, local;
int i, n, rawhit;
raw = search.raw;
foldascii(&raw);
transstr(im.l, nil, &search.raw, &search.text);
local = search.text;
foldascii(&local);
clearkouho();
n = dictprefix(getlang(LangEMOJI)->dict, &raw, kouho, Maxkouho);
rawhit = n != 0;
for(i = 0; i < n; i++)
addkouho(&kouho[i]);
if(scmp(&raw, &local) != 0){
n = dictprefix(getlang(LangEMOJI)->dict, &local, kouho, Maxkouho);
for(i = 0; i < n; i++)
addkouho(&kouho[i]);
}
if(rawhit)
search.text = search.raw;
selectfirst();
}
/*
* The keys typed since Ctrl+H go on composing from the seed. A reading
* is also the start of the longer words it begins, so its own
* conversions come first and theirs follow: 대한 answers 大寒 and, past
* them, 大韓民國.
*/
static void
hanjaquery(void)
{
Str kouho[Maxkouho], tail;
int i, n;
transstr(im.l, &search.seed, &search.raw, &tail);
search.text = search.back;
sappend(&search.text, &tail);
clearkouho();
n = dictprefix(getlang(LangHANJA)->dict, &search.text, kouho, Maxkouho);
for(i = 0; i < n; i++)
addkouho(&kouho[i]);
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);
modemark = 0;
okuriat = -1;
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)
{
Str pre;
if(search.lang){
searchpre(&pre);
sappend(com, &pre);
}else if(im.sel >= 0)
sappend(com, &im.kouho[im.sel]);
else
commitim(&im, com);
reset();
}
/*
* Focus can be lost after the next context's keys arrive, so the engine
* changes hands with text still pending. The context it was taken from
* gets that text back when it is next heard from.
*/
static void
takelost(void *owner, Str *com)
{
if(owner != lostowner)
return;
sappend(com, &lost);
lostowner = nil;
}
/* The syllables from i to the end of the client's text, then seed. */
static void
reachkey(int i, Str *seed, Str *key)
{
sclear(key);
for(; i < surround.n; i++)
sputr(key, surround.r[i]);
sappend(key, seed);
}
/*
* Ctrl+H converts a word, and a word is committed one syllable at a
* time: only its last syllable is still pending. So the reading reaches
* back into the text the client already has, as far as the dictionary
* still knows the whole of it -- 한자 converts as a word where the 한 is
* the client's and only the 자 is ours. Nothing but the dictionary says
* where to stop: a reading is syllables, so a key holding a space or a
* Hanja leads nowhere and the reach ends there.
*/
static void
reachback(Str *seed, Str *back)
{
Str key;
Trie *t;
int i, best;
t = getlang(LangHANJA)->dict;
best = surround.n;
for(i = surround.n - 1; i >= 0; i--){
if(surround.n - i + seed->n > Maxrunes)
break;
reachkey(i, seed, &key);
if(trienode(t, &key) >= 0)
best = i;
}
sclear(back);
for(i = best; i < surround.n; i++)
sputr(back, surround.r[i]);
}
/*
* 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 back, seed;
sclear(&back);
sclear(&seed);
if(lang == LangHANJA && !search.lang && im.sel < 0){
impre(&im, &seed);
/* Nothing pending is no reading, and reaches into nothing. */
if(seed.n != 0)
reachback(&seed, &back);
}else
flush(com);
reset();
search.lang = lang;
search.back = back;
search.seed = seed;
if(lang == LangHANJA)
searchquery();
}
/* The client keeps what the reading reached back into; we take it back. */
static void
picksearch(int n, Str *com)
{
takeback = search.back.n;
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 || ks == ' '){
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.back.n != 0)
sclear(&search.back); /* the reach undone, in its turn */
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)
{
Str mark;
int n;
Rune c;
modemark = 0; /* any key after the switch takes its mark away */
/* Shift on a romaji letter marks the okurigana, as SKK's capital does. */
if(isjp(&im) && (mod & Mshift) && ks >= 'A' && ks <= 'Z' && haspre(&im)){
impre(&im, &mark);
okuriat = mark.n;
}
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;
}
/* Ctrl+Backspace deletes a word, Ctrl+Tab changes tab: not ours. */
if(ks >= Kspec && (mod & ~Mshift)){
flush(com);
return 0;
}
if(search.lang)
return searchkey(ks, mod, com);
n = movedelta(ks);
if(n != 0 && im.nkouho != 0){
movekouho(n);
return 1;
}
/* Digits pick a candidate only once the list has been engaged. */
n = im.sel >= 0 ? numberedkouho(ks, mod) : -1;
if(n >= 0){
sappend(com, &im.kouho[n]);
reset();
return 1;
}
if((ks == ' ' || ks == Ktab) && isjp(&im) && haspre(&im) &&
!(mod & ~Mshift)){
/* Space and Tab convert: they step through the candidates,
* Katakana last. Katakana mode converts nothing, so there
* the key only commits, and then it is the application's:
* the space between two Katakana words is a space. */
katakouho();
if(im.nkouho == 0){
flush(com);
return 0;
}
cyclekouho(mod & Mshift ? -1 : 1);
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 || ks == Kesc){
if(!haspre(&im))
return 0;
if(im.sel >= 0)
im.sel = -1; /* back from the candidate to the reading */
else if(ks == Kesc)
reset();
else{
okuriat = -1; /* the mark goes with the kana it marked */
im.l->back(&im);
if(haspre(&im))
dictqjp();
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);
modemark = 0;
okuriat = -1;
activeowner = nil;
lostowner = nil;
clientpre = 0;
sclear(&surround);
takeback = 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;
takeback = 0;
switch(kr->op){
case Keyrelease:
takelost(kr->owner, &res.commit);
if(kr->owner == activeowner){
flush(&res.commit);
activeowner = nil;
clientpre = 0;
}
break;
case Keyreset:
takelost(kr->owner, &res.commit);
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)
clientpre = kr->clientpre;
break;
case Keypress:
res.eaten = 0;
takelost(kr->owner, &res.commit);
if(kr->owner == activeowner)
clientpre = kr->clientpre;
if(keymeaningful(kr->ks)){
if(kr->owner != activeowner){
sclear(&lost);
flush(&lost);
lostowner = lost.n > 0 ? activeowner : nil;
activeowner = kr->owner;
clientpre = kr->clientpre;
}
caret = kr->caret;
surround = kr->surround;
res.eaten = transition(kr->ks, kr->mod, &res.commit);
}
break;
}
redraw();
if(kr->owner == activeowner){
if(search.lang)
searchpre(&res.preedit);
else if(im.sel >= 0)
res.preedit = im.kouho[im.sel]; /* what Enter would commit */
else
impre(&im, &res.preedit);
}
res.del = takeback;
chansend(kr->reply, &res);
}
void
imthread(void*)
{
Keyreq kr;
threadsetname("im");
init();
for(;;){
chanrecv(keyc, &kr);
imhandlekey(&kr);
}
}