Files
strans/strans.c
Hojun-Cho a70cfc42e3 xim: drop what no client uses; poll for owner loss like IBus
XIM text always went out through imdkit's COMPOUND_TEXT converter,
which already wraps every UTF-8 string in ESC%G; the UTF8_STRING
negotiation, the per-client encoding list, ximtext.c's fallback and its
wrapper never changed a byte on the wire. The spot location is honoured
for every style now (GTK's XIM module sends it with PreeditCallbacks)
and an unset focus window means the client window, as the spec says,
so those clients get the popup at the caret. readattrs/place kept four
transient fields to pass values between them; ximclose tore down state
right before die(); the OOM passthrough context was a third policy for
one small calloc where emalloc dies like everything else. Both frontends
now poll for engine-owner loss only while a preedit shows instead of
XIM waking on a pipe the engine had to know about; ibus stops waking
five times a second when idle. keymeaningful() is the engine's own
predicate. The standalone xim_test moved into the unit suite, so
xim/Makefile is gone.
2026-08-16 16:16:31 +09:00

848 lines
14 KiB
C

#include "dat.h"
#include "fn.h"
static Im im;
static void *activeowner;
static int activecap;
static Caret caret;
static int candidatechosen;
static Drawcmd lastdraw;
static Emit transjp(Im*, Rune);
static void backjp(Im*);
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);
/* 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;
}
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)
{
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;
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 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);
}
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 *p)
{
if(p->raw.n != 0)
spopr(&p->raw);
else
spopr(&p->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(mapmatch(p->l->map, &key, &mapped) != TrieMiss){
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(mapmatch(p->l->map, &key, &mapped) != TrieMiss){
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;
}
/*
* 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. */
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)
{
return c == LangEMOJI || c == LangHANJA ? c : 0;
}
static void
endsearch(void)
{
search.lang = 0;
sclear(&search.raw);
sclear(&search.text);
clearkouho();
}
/* A search commits any pending composition and takes over the keys. */
static void
startsearch(int lang, Str *com)
{
commit(com);
endsearch();
search.lang = lang;
}
static void
commitsearch(Str *com)
{
sappend(com, &search.text);
endsearch();
}
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)
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)
picksearch(max(im.sel, 0), com);
else
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);
if(n == search.lang)
endsearch();
else if(n != 0)
startsearch(n, com);
else{
commitsearch(com);
return setlang(c);
}
return 1;
}
if(search.raw.n >= Maxrunes){
commitsearch(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);
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);
if(n != 0){
startsearch(n, com);
return 1;
}
commit(com);
reset();
return setlang(c);
}
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;
}
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;
candidatechosen = 0;
memset(&lastdraw, 0, sizeof lastdraw);
}
/*
* The engine belongs to whichever context last typed a real key; only
* the owner's requests change state. 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){
reset();
activeowner = nil;
activecap = 0;
}
break;
case Keyreset:
if(kr->owner == activeowner)
reset();
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(&res.preedit);
}
chansend(kr->reply, &res);
}
void
imthread(void*)
{
Keyreq kr;
threadsetname("im");
init();
for(;;){
chanrecv(keyc, &kr);
imhandlekey(&kr);
}
}