Compare commits

..

11 Commits

Author SHA1 Message Date
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
761428dbc4 engine: a Hanja word shows once, as every other candidate does
hanjaquery was the only query that assigned dictprefix's output straight
into im.kouho; emojiquery, dictqjp and katakouho all go through
addkouho, which is the only place a duplicate is dropped.  Two readings
under one prefix can carry the same word, and 198 of the 61,479 one- and
two-syllable prefixes in hanja.dict do: 게 listed 偈句 at rows 8 and 9,
개근 listed 皆勤狀 twice in five.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:20:18 +09:00
1bc0ba0491 build: the parent make knows what the daemon is built from
../strans listed no prerequisites, so once the file existed make held it
up to date whatever changed under it: touch strans.c, run
make -C tests check-live, and it planned no compile at all and smoked a
stale daemon.  The sibling rule for the GTK module answered it by naming
three of its sources, which is the parent Makefile's list copied and
already short of gtk/Makefile.  Neither list is needed -- ask the parent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:18:43 +09:00
74607afef8 test: the take-back byte is read back off the wire
ipc_response_fragmented_and_truncated already packed del = 2 and sent
the frame a byte at a time; it checked the commit and the preedit and
never looked at the field the v2 wire was widened for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:17:27 +09:00
591029237e test: a fixture that cannot fail needs no guard
ximbegin took a struct ct* only to USED() it and always returned 1, so
its seven `if(!ximbegin(...)) goto cleanup;` call sites tested nothing
and three of the cleanup labels they jumped to were unreachable.
ibusbegin malloc'd one byte twice so that its two fake DBusConnections
would differ by address, then CT_CHECKed the mallocs; two bytes in the
fixture are two addresses, and nothing has to be freed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:16:02 +09:00
bdf05e3c4a str, bench: three checks that guard nothing
sinit sets s->n = 0 as its first statement and no failure path restores
it, so stail's sclear after a failed sinit could never change anything;
say so in sinit's comment instead, where the contract belongs.  bench
compared a uintmax_t against UINT64_MAX and a size_t against UINT64_MAX,
both constant on any host this builds for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:14:16 +09:00
4802b6593e wl: counting bytes needs no buffer to put them in
backbytes copied the runes into a Str and encoded them into a 257-byte
stack buffer only to throw the bytes away and keep the length.  runelen
answers over the same runes with neither.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:13:13 +09:00
ba0a08abe2 docs: Backspace gives the reach back
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:05:30 +09:00
08d0e0fec8 engine: an okurigana mark does not outlive its kana
Shift on a romaji letter marks where the okurigana begins, and Backspace
shortened the reading through im.l->back without clearing the mark.
dictqjp only applies a mark that still falls inside the reading, so the
stale index lay dormant while the reading was short and fired again the
moment it grew back past it: kaKu, Backspace, then nzi offered
噛んじ 兼んじ 漢字 幹事 感じ, where kanzi typed plainly offers 漢字 first.
A Backspace edits the reading, so the split marked in it is gone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:03:15 +09:00
d3d46c8cb1 engine: Backspace gives the reach back before what was typed
searchkey popped search.raw and then search.seed but never touched
search.back, so once the typed syllable was gone the query was nothing
but the client's own text: an empty preedit above a full candidate list
drawn from syllables the user had not selected, and Enter rewrote them.
With 상 written and 태 typed, Ctrl+H offered 狀態; one Backspace left
the query 상 and 128 candidates, and Enter replaced the 상 with 上 and
dropped the 태 altogether.

Undo now runs backwards through what happened -- the keys typed since
Ctrl+H, then the reach Ctrl+H made, then the syllable that seeded it --
so the same Backspace narrows 상태 to 태 and offers 太, which is also
the only way there was ever going to be to convert the syllable alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:02:52 +09:00
b08f3bb7a2 engine: nothing pending is no reading, and reaches into nothing
startsearch let reachback walk the client's text whenever Ctrl+H found
im.sel < 0, without asking whether anything was pending, so a Ctrl+H
pressed to begin a reading took the syllables already written instead.
With the cursor after 입니다 the query became 다, the preedit showed
nothing at all, and Space committed 多 over the 다 the user had written;
typing the reading the key was pressed for gave 다한, so 입니다漢 came
out 입니多恨.  A reading is what is pending, and there is none.

The seeded reach is untouched: 한 committed with 자 pending still
converts 한자 as a word and takes the 한 back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:02:27 +09:00
10 changed files with 127 additions and 69 deletions

View File

@@ -33,16 +33,17 @@ Japanese composes a whole reading and then offers its Kanji with none
chosen. `Space` and `Tab` step through the candidates and wrap (`Shift` chosen. `Space` and `Tab` step through the candidates and wrap (`Shift`
reverses), the reading in Katakana last, so a word the dictionary lacks reverses), the reading in Katakana last, so a word the dictionary lacks
converts with one `Space`; `Up`/`Down` and `PageUp`/`PageDown` move without converts with one `Space`; `Up`/`Down` and `PageUp`/`PageDown` move without
wrapping. `Enter` commits the chosen candidate, or the reading, as `0` and wrapping. A chosen candidate takes the reading's place in the preedit, so
typing on always do. Once a candidate is chosen `1`-`9` take that row of it stands where it will land, and `Enter` commits it, or the reading, as
the page shown; before that the digits type. `Backspace` deletes the last `0` and typing on always do. Once a candidate is chosen `1`-`9` take that
kana shown, and romaji that made no kana stays as typed until it is mended; row of the page shown; before that the digits type. `Backspace` deletes
`Esc` cancels the reading, and on a chosen candidate both go back to it. the last kana shown, and romaji that made no kana stays as typed until it
The dictionary keys verbs and adjectives by stem and okurigana, so `kaku` is mended; `Esc` cancels the reading, and on a chosen candidate both go
offers かく's readings and then 書く and its kin — typing the okurigana back to it. The dictionary keys verbs and adjectives by stem and
with `Shift`, as SKK does, `kaKu`, asks for that split first. Katakana okurigana, so `kaku` offers かく's readings and then 書く and its kin —
mode does not convert, so there `Space` and `Tab` commit the reading and typing the okurigana with `Shift`, as SKK does, `kaKu`, asks for that
go on to the application. split first. Katakana mode does not convert, so there `Space` and `Tab`
commit the reading and go on to the application.
Korean composes one syllable at a time. `Enter`, `Tab`, `Esc` and any key Korean composes one syllable at a time. `Enter`, `Tab`, `Esc` and any key
that is not a jamo commit the syllable and go on to the application, so that is not a jamo commit the syllable and go on to the application, so
@@ -60,12 +61,15 @@ a syllable — 한자 gives 漢字, 대한민국 gives 大韓民國 — and `Esc
syllable back. A word is committed a syllable at a time, so the reading syllable back. A word is committed a syllable at a time, so the reading
also reaches back into the text the application already holds, as far as also reaches back into the text the application already holds, as far as
the dictionary knows the whole of it: type 한자 and then `Ctrl+H` and the the dictionary knows the whole of it: type 한자 and then `Ctrl+H` and the
query is 한자, not 자, and picking 漢字 takes the 한 back. A reading is query is 한자, not 자, and picking 漢字 takes the 한 back. `Backspace`
also the start of the longer words it begins, whose conversions follow gives that reach back before it deletes what was typed, so 상 written
its own, so 대한 answers 大寒 first and 大韓民國 further down. A lone with 태 typed answers 狀態, and one `Backspace` answers 態 for the
consonant is a reading too, and answers with the symbol table a Korean syllable alone. A reading is also the start of the longer words it
keyboard's 한자 key has always offered: ㅁ gives ※ ○ △ ㈜, ㄴ the brackets begins, whose conversions follow its own, so 대한 answers 大寒 first and
「」『』, ㄹ the units ℃ ㎏ , ㅇ the circled numbers ①②③. 大韓民國 further down. A lone consonant is a reading too, and answers
with the symbol table a Korean keyboard's 한자 key has always offered:
ㅁ gives ※ ○ △ ㈜, ㄴ the brackets 「」『』, ㄹ the units ℃ ㎏ , ㅇ the
circled numbers ①②③.
Reaching back needs the application to hand over the text around its Reaching back needs the application to hand over the text around its
cursor and to take some of it away again. The Wayland input method, IBus cursor and to take some of it away again. The Wayland input method, IBus

View File

@@ -99,7 +99,7 @@ iterations(char *s)
goto Bad; goto Bad;
errno = 0; errno = 0;
n = strtoumax(s, &end, 10); n = strtoumax(s, &end, 10);
if(errno != 0 || *end != '\0' || n == 0 || n > UINT64_MAX) if(errno != 0 || *end != '\0' || n == 0)
goto Bad; goto Bad;
return n; return n;
Bad: Bad:
@@ -157,7 +157,7 @@ main(int argc, char **argv)
loadkeys(argv[1]); loadkeys(argv[1]);
niter = argc == 3 ? iterations(argv[2]) : 1000; niter = argc == 3 ? iterations(argv[2]) : 1000;
if(nkeys > UINT64_MAX || niter > UINT64_MAX / nkeys){ if(niter > UINT64_MAX / nkeys){
fprintf(stderr, "iteration count overflows key total\n"); fprintf(stderr, "iteration count overflows key total\n");
exit(1); exit(1);
} }

6
str.c
View File

@@ -1,7 +1,8 @@
#include "dat.h" #include "dat.h"
#include "fn.h" #include "fn.h"
/* Fills s from n bytes of UTF-8; whole, valid, and at most Maxrunes. */ /* Fills s from n bytes of UTF-8; whole, valid, and at most Maxrunes.
* Anything else leaves s empty. */
int int
sinit(Str *s, char *src, int n) sinit(Str *s, char *src, int n)
{ {
@@ -35,8 +36,7 @@ stail(Str *s, char *src, int n)
nr = utfnlen(src, n); nr = utfnlen(src, n);
for(p = src; nr > Maxrunes; nr--) for(p = src; nr > Maxrunes; nr--)
p += chartorune(&r, p); p += chartorune(&r, p);
if(!sinit(s, p, n - (p - src))) sinit(s, p, n - (p - src));
sclear(s);
} }
void void

View File

@@ -653,14 +653,16 @@ emojiquery(void)
static void static void
hanjaquery(void) hanjaquery(void)
{ {
Str tail; Str kouho[Maxkouho], tail;
int i, n;
transstr(im.l, &search.seed, &search.raw, &tail); transstr(im.l, &search.seed, &search.raw, &tail);
search.text = search.back; search.text = search.back;
sappend(&search.text, &tail); sappend(&search.text, &tail);
clearkouho(); clearkouho();
im.nkouho = dictprefix(getlang(LangHANJA)->dict, &search.text, n = dictprefix(getlang(LangHANJA)->dict, &search.text, kouho, Maxkouho);
im.kouho, Maxkouho); for(i = 0; i < n; i++)
addkouho(&kouho[i]);
selectfirst(); selectfirst();
} }
@@ -793,7 +795,9 @@ startsearch(int lang, Str *com)
sclear(&seed); sclear(&seed);
if(lang == LangHANJA && !search.lang && im.sel < 0){ if(lang == LangHANJA && !search.lang && im.sel < 0){
impre(&im, &seed); impre(&im, &seed);
reachback(&seed, &back); /* Nothing pending is no reading, and reaches into nothing. */
if(seed.n != 0)
reachback(&seed, &back);
}else }else
flush(com); flush(com);
reset(); reset();
@@ -846,6 +850,8 @@ searchkey(u32int ks, u32int mod, Str *com)
if(ks == Kback){ if(ks == Kback){
if(search.raw.n != 0) if(search.raw.n != 0)
spopr(&search.raw); spopr(&search.raw);
else if(search.back.n != 0)
sclear(&search.back); /* the reach undone, in its turn */
else if(search.seed.n != 0) else if(search.seed.n != 0)
spopr(&search.seed); spopr(&search.seed);
else{ else{
@@ -966,6 +972,7 @@ transition(u32int ks, u32int mod, Str *com)
else if(ks == Kesc) else if(ks == Kesc)
reset(); reset();
else{ else{
okuriat = -1; /* the mark goes with the kana it marked */
im.l->back(&im); im.l->back(&im);
if(haspre(&im)) if(haspre(&im))
dictqjp(); dictqjp();
@@ -1090,6 +1097,8 @@ imhandlekey(Keyreq *kr)
if(kr->owner == activeowner){ if(kr->owner == activeowner){
if(search.lang) if(search.lang)
searchpre(&res.preedit); searchpre(&res.preedit);
else if(im.sel >= 0)
res.preedit = im.kouho[im.sel]; /* what Enter would commit */
else else
impre(&im, &res.preedit); impre(&im, &res.preedit);
} }

View File

@@ -89,7 +89,8 @@ gtk_live_test: gtk_live_test.c $(LIVESRC) ../ipc.c ../ipc.h
$(LDFLAGS) -o $@ gtk_live_test.c live.c ../ipc.c $(GTK_LIBS) \ $(LDFLAGS) -o $@ gtk_live_test.c live.c ../ipc.c $(GTK_LIBS) \
-pthread $(LDLIBS) -pthread $(LDLIBS)
../gtk/im-strans.so: ../gtk/main.c ../ipc.c ../ipc.h # The parent make knows what these are built from; always ask it.
../gtk/im-strans.so:
$(MAKE) -C ../gtk $(MAKE) -C ../gtk
../strans: ../strans:
@@ -145,4 +146,5 @@ unit_%.o: ../%.c ../dat.h ../fn.h ../ipc.h
clean: clean:
rm -f $(OBJS) $(PROG) $(STRESS) $(LIVE) rm -f $(OBJS) $(PROG) $(STRESS) $(LIVE)
.PHONY: all check check-live check-stress test clean .PHONY: all check check-live check-stress test clean \
../strans ../gtk/im-strans.so

View File

@@ -411,6 +411,7 @@ engine_commit_contract(struct ct *t)
{ 'd', "", "ㅇㅇㅇ" }, { 'd', "", "ㅇㅇㅇ" },
{ 'k', "", "ㅏㅏㅏ" }, { 'k', "", "ㅏㅏㅏ" },
}; };
Drawcmd dc;
Keyres res; Keyres res;
Str all, com; Str all, com;
char owner; char owner;
@@ -495,6 +496,26 @@ engine_commit_contract(struct ct *t)
CT_CHECK(t, !keystroke(0xf008, 0, &com)); CT_CHECK(t, !keystroke(0xf008, 0, &com));
checkstr(t, "non-ASCII key commit", "", &com); checkstr(t, "non-ASCII key commit", "", &com);
CT_EQ_INT(t, 0, im.pre.n); CT_EQ_INT(t, 0, im.pre.n);
/* A chosen candidate is what the client shows and what Enter
* commits; the popup keeps the reading above the list. */
init();
im.l = getlang(LangJP);
draindraw(nil);
ownerrequestcap(&owner, 1, Keypress, 'k', 0);
res = ownerrequestcap(&owner, 1, Keypress, 'a', 0);
checkstr(t, "the reading before a candidate", "", &res.preedit);
im.kouho[0] = mkstr("");
im.nkouho = 1;
res = ownerrequestcap(&owner, 1, Keypress, Kdown, 0);
checkstr(t, "the candidate the client shows", "", &res.preedit);
clientpre = 0;
redraw();
if(CT_CHECK(t, draindraw(&dc) > 0))
checkstr(t, "the popup keeps the reading", "", &dc.pre);
res = ownerrequestcap(&owner, 1, Keypress, Kret, 0);
checkstr(t, "Enter commits what was shown", "", &res.commit);
CT_EQ_INT(t, 0, res.preedit.n);
} }
void void
@@ -1277,6 +1298,13 @@ engine_japanese_candidates(struct ct *t)
checkstr(t, "then the second", "描く", &im.kouho[1]); checkstr(t, "then the second", "描く", &im.kouho[1]);
checkstr(t, "then the reading's own", "", &im.kouho[2]); checkstr(t, "then the reading's own", "", &im.kouho[2]);
} }
/* Backspace edits the reading, so the split it marked is gone. */
CT_CHECK(t, keystroke(Kback, 0, &com));
CT_CHECK(t, keystroke('k', 0, &com));
CT_CHECK(t, keystroke('u', 0, &com));
if(CT_EQ_INT(t, 3, im.nkouho))
checkstr(t, "the mark did not outlive its kana", "",
&im.kouho[0]);
CT_CHECK(t, keystroke(Kesc, 0, &com)); CT_CHECK(t, keystroke(Kesc, 0, &com));
setdict(jp->dict, "いt", "言 行"); setdict(jp->dict, "いt", "言 行");
setdict(jp->dict, "いっt", "言 行"); setdict(jp->dict, "いっt", "言 行");
@@ -2005,6 +2033,37 @@ engine_hanja_reaches_back(struct ct *t)
CT_EQ_INT(t, 0, takeback); CT_EQ_INT(t, 0, takeback);
} }
/* Backspace gives the reach back before it eats what was typed. */
surround = mkstr("");
im.pre = mkstr("");
sclear(&com);
CT_CHECK(t, keystroke('h', Mctrl, &com));
checkstr(t, "the reach joined the reading", "민국", &search.text);
CT_CHECK(t, keystroke(Kback, 0, &com));
checkstr(t, "the client's syllable goes back first", "",
&search.text);
CT_EQ_INT(t, 0, search.back.n);
sclear(&com);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "only what was typed is committed", "", &com);
CT_EQ_INT(t, 0, takeback);
/* Nothing pending is no reading, and reaches into nothing. */
surround = mkstr("");
sclear(&im.pre);
sclear(&com);
CT_CHECK(t, keystroke('h', Mctrl, &com));
CT_EQ_INT(t, 0, search.back.n);
CT_EQ_INT(t, 0, im.nkouho);
if(typekeys(t, "als", &com)){
checkstr(t, "the reading typed is the whole query", "",
&search.text);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "the client's syllable is not converted", "",
&com);
CT_EQ_INT(t, 0, takeback);
}
/* The frontend's own request carries the text and is told to take it. */ /* The frontend's own request carries the text and is told to take it. */
init(); init();
im.l = getlang(LangKO); im.l = getlang(LangKO);
@@ -2034,6 +2093,8 @@ engine_hanja_word_prefix(struct ct *t)
Str com; Str com;
hanjabegin(&f, LangKO); hanjabegin(&f, LangKO);
/* Two readings under one prefix may share a word; it shows once. */
setdict(f.dictlang->dict, "민민", "");
sclear(&com); sclear(&com);
if(!CT_CHECK(t, keystroke('h', Mctrl, &com)) || if(!CT_CHECK(t, keystroke('h', Mctrl, &com)) ||
!typekeys(t, "als", &com)) !typekeys(t, "als", &com))

View File

@@ -13,6 +13,8 @@ struct Ibusfix
{ {
Channel *oldreply; Channel *oldreply;
Pump pump; Pump pump;
/* A fake connection is nothing but an address to tell two apart. */
char fake[2];
DBusConnection *c1; DBusConnection *c1;
DBusConnection *c2; DBusConnection *c2;
}; };
@@ -81,8 +83,8 @@ cleanup:
CT_CHECK(t, rmdir(root) == 0); CT_CHECK(t, rmdir(root) == 0);
} }
static int static void
ibusbegin(struct ct *t, Ibusfix *f) ibusbegin(Ibusfix *f)
{ {
Drawcmd dc; Drawcmd dc;
@@ -97,9 +99,8 @@ ibusbegin(struct ct *t, Ibusfix *f)
f->oldreply = replyc; f->oldreply = replyc;
replyc = chancreate(sizeof(Keyres), 0); replyc = chancreate(sizeof(Keyres), 0);
pumpstart(&f->pump, Maxcontexts); pumpstart(&f->pump, Maxcontexts);
f->c1 = malloc(1); f->c1 = (DBusConnection*)&f->fake[0];
f->c2 = malloc(1); f->c2 = (DBusConnection*)&f->fake[1];
return CT_CHECK(t, f->c1 != nil && f->c2 != nil);
} }
static void static void
@@ -119,8 +120,6 @@ ibusend(Ibusfix *f)
testengineinit(LangEN); testengineinit(LangEN);
while(channbrecv(drawc, &dc) > 0) while(channbrecv(drawc, &dc) > 0)
; ;
free(f->c1);
free(f->c2);
chanfree(replyc); chanfree(replyc);
replyc = f->oldreply; replyc = f->oldreply;
} }
@@ -168,8 +167,7 @@ ibus_capability_policy(struct ct *t)
Ictx *a, *b; Ictx *a, *b;
Keyres res; Keyres res;
if(!ibusbegin(t, &f)) ibusbegin(&f);
goto cleanup;
a = newcontext(f.c1, "/context/cap-a"); a = newcontext(f.c1, "/context/cap-a");
b = newcontext(f.c2, "/context/cap-b"); b = newcontext(f.c2, "/context/cap-b");
if(!CT_CHECK(t, a != nil && b != nil)) if(!CT_CHECK(t, a != nil && b != nil))
@@ -223,8 +221,7 @@ ibus_private_input_policy(struct ct *t)
char text[Maxutf]; char text[Maxutf];
int i; int i;
if(!ibusbegin(t, &f)) ibusbegin(&f);
goto cleanup;
ctx = newcontext(f.c1, "/context/private"); ctx = newcontext(f.c1, "/context/private");
if(!CT_CHECK(t, ctx != nil)) if(!CT_CHECK(t, ctx != nil))
goto cleanup; goto cleanup;
@@ -274,8 +271,7 @@ ibus_context_lifecycle(struct ct *t)
Caret at; Caret at;
char text[Maxutf]; char text[Maxutf];
if(!ibusbegin(t, &f)) ibusbegin(&f);
goto cleanup;
a = newcontext(f.c1, "/context/one"); a = newcontext(f.c1, "/context/one");
b = newcontext(f.c2, "/context/one"); b = newcontext(f.c2, "/context/one");
if(!CT_CHECK(t, a != nil && b != nil && a != b)) if(!CT_CHECK(t, a != nil && b != nil && a != b))
@@ -387,8 +383,7 @@ ibus_active_release_lifecycle(struct ct *t)
Ictx *ctx, *reused; Ictx *ctx, *reused;
Keyres res; Keyres res;
if(!ibusbegin(t, &f)) ibusbegin(&f);
goto cleanup;
ctx = newcontext(f.c1, "/context/reset"); ctx = newcontext(f.c1, "/context/reset");
if(!CT_CHECK(t, ctx != nil)) if(!CT_CHECK(t, ctx != nil))
goto cleanup; goto cleanup;

View File

@@ -302,6 +302,7 @@ ipc_response_fragmented_and_truncated(struct ct *t)
} }
if(i == n && CT_EQ_INT(t, 0, ipcreadresp(fd[1], 1, commit, preedit, if(i == n && CT_EQ_INT(t, 0, ipcreadresp(fd[1], 1, commit, preedit,
&resp))){ &resp))){
CT_EQ_INT(t, 2, resp.del);
CT_EQ_STR(t, "abc", commit); CT_EQ_STR(t, "abc", commit);
CT_EQ_STR(t, "xy", preedit); CT_EQ_STR(t, "xy", preedit);
} }

View File

@@ -398,8 +398,8 @@ struct Freejob
Channel *done; Channel *done;
}; };
static int static void
ximbegin(struct ct *t, Ximfix *f, int lang) ximbegin(Ximfix *f, int lang)
{ {
Drawcmd dc; Drawcmd dc;
@@ -410,8 +410,6 @@ ximbegin(struct ct *t, Ximfix *f, int lang)
f->oldreply = replyc; f->oldreply = replyc;
replyc = chancreate(sizeof(Keyres), 0); replyc = chancreate(sizeof(Keyres), 0);
pumpstart(&f->pump, 16); pumpstart(&f->pump, 16);
USED(t);
return 1;
} }
static void static void
@@ -466,8 +464,7 @@ xim_adapter_key_contract(struct ct *t)
u32int allmod; u32int allmod;
memset(&state, 0, sizeof state); memset(&state, 0, sizeof state);
if(!ximbegin(t, &f, LangJP)) ximbegin(&f, LangJP);
goto cleanup;
allmod = ~0; allmod = ~0;
keypress(&state, 'x', allmod, &res); keypress(&state, 'x', allmod, &res);
req = nexttrace(t, &f, Keypress, &state); req = nexttrace(t, &f, Keypress, &state);
@@ -487,7 +484,6 @@ xim_adapter_key_contract(struct ct *t)
CT_CHECK(t, !res.eaten); CT_CHECK(t, !res.eaten);
checkstr(t, "preedit", "", &res.commit); checkstr(t, "preedit", "", &res.commit);
checkstr(t, "preedit", "", &res.preedit); checkstr(t, "preedit", "", &res.preedit);
cleanup:
ximend(&f); ximend(&f);
} }
@@ -502,8 +498,7 @@ xim_adapter_release_lifecycle(struct ct *t)
memset(&a, 0, sizeof a); memset(&a, 0, sizeof a);
memset(&b, 0, sizeof b); memset(&b, 0, sizeof b);
dead = nil; dead = nil;
if(!ximbegin(t, &f, LangJP)) ximbegin(&f, LangJP);
goto cleanup;
keypress(&a, 'k', 0, &res); keypress(&a, 'k', 0, &res);
nexttrace(t, &f, Keypress, &a); nexttrace(t, &f, Keypress, &a);
keypress(&a, 'a', 0, &res); keypress(&a, 'a', 0, &res);
@@ -580,8 +575,7 @@ xim_adapter_free_waits_for_release(struct ct *t)
state = nil; state = nil;
freed = nil; freed = nil;
workeractive = 0; workeractive = 0;
if(!ximbegin(t, &f, LangJP)) ximbegin(&f, LangJP);
goto cleanup;
state = calloc(1, sizeof *state); state = calloc(1, sizeof *state);
freed = chancreate(sizeof(uchar), 0); freed = chancreate(sizeof(uchar), 0);
if(!CT_CHECK(t, state != nil && freed != nil)) if(!CT_CHECK(t, state != nil && freed != nil))
@@ -736,8 +730,7 @@ xim_adapter_placement_updates(struct ct *t)
ic = (xcb_im_input_context_t*)(uintptr)71; ic = (xcb_im_input_context_t*)(uintptr)71;
memset(&state, 0, sizeof state); memset(&state, 0, sizeof state);
oldstate = kstate; oldstate = kstate;
if(!ximbegin(t, &f, LangJP)) ximbegin(&f, LangJP);
goto cleanup;
kstate = testkeystate("us"); kstate = testkeystate("us");
if(!CT_CHECK(t, kstate != nil)) if(!CT_CHECK(t, kstate != nil))
goto cleanup; goto cleanup;
@@ -891,8 +884,7 @@ xim_adapter_callback_cleanup(struct ct *t)
ic = (xcb_im_input_context_t*)(uintptr)29; ic = (xcb_im_input_context_t*)(uintptr)29;
memset(&state, 0, sizeof state); memset(&state, 0, sizeof state);
if(!ximbegin(t, &f, LangJP)) ximbegin(&f, LangJP);
return;
wireclear(); wireclear();
wirebind(0, ic, &state); wirebind(0, ic, &state);
state.xic = ic; state.xic = ic;
@@ -980,8 +972,7 @@ xim_adapter_callback_transfer(struct ct *t)
aic = (xcb_im_input_context_t*)(uintptr)50; aic = (xcb_im_input_context_t*)(uintptr)50;
memset(&a, 0, sizeof a); memset(&a, 0, sizeof a);
memset(&b, 0, sizeof b); memset(&b, 0, sizeof b);
if(!ximbegin(t, &f, LangJP)) ximbegin(&f, LangJP);
goto cleanup;
wireclear(); wireclear();
a.xic = aic; a.xic = aic;
a.clientpre = 1; a.clientpre = 1;
@@ -996,7 +987,6 @@ xim_adapter_callback_transfer(struct ct *t)
CT_EQ_PTR(t, nil, preowner); CT_EQ_PTR(t, nil, preowner);
release(&b); release(&b);
nexttrace(t, &f, Keyrelease, &b); nexttrace(t, &f, Keyrelease, &b);
cleanup:
preowner = nil; preowner = nil;
ximend(&f); ximend(&f);
} }
@@ -1016,8 +1006,7 @@ xim_adapter_callback_owner_loss(struct ct *t)
ic = (xcb_im_input_context_t*)(uintptr)51; ic = (xcb_im_input_context_t*)(uintptr)51;
memset(&state, 0, sizeof state); memset(&state, 0, sizeof state);
foreign = 0; foreign = 0;
if(!ximbegin(t, &f, LangJP)) ximbegin(&f, LangJP);
goto cleanup;
wireclear(); wireclear();
wirebind(0, ic, &state); wirebind(0, ic, &state);
state.xic = ic; state.xic = ic;
@@ -1086,7 +1075,6 @@ xim_adapter_callback_owner_loss(struct ct *t)
CT_CHECK(t, channbrecv(f.pump.trace, &trace) > 0); CT_CHECK(t, channbrecv(f.pump.trace, &trace) > 0);
CT_EQ_INT(t, Keyrelease, trace.op); CT_EQ_INT(t, Keyrelease, trace.op);
CT_EQ_PTR(t, &foreign, trace.owner); CT_EQ_PTR(t, &foreign, trace.owner);
cleanup:
preowner = nil; preowner = nil;
ximend(&f); ximend(&f);
} }

10
wl.c
View File

@@ -127,14 +127,12 @@ sendrequest(int op, u32int ks, u32int mod, Keyres *res)
static int static int
backbytes(int n) backbytes(int n)
{ {
char utf[Maxutf]; int i, nb;
Str s;
int i;
sclear(&s); nb = 0;
for(i = max(surround.n - n, 0); i < surround.n; i++) for(i = max(surround.n - n, 0); i < surround.n; i++)
sputr(&s, surround.r[i]); nb += runelen(surround.r[i]);
return stoutf(&s, utf, sizeof utf); return nb;
} }
/* The engine's modifier bits, read from the grab's own keyboard state. */ /* The engine's modifier bits, read from the grab's own keyboard state. */