From 8ebe6b32df47d88d39a3e498b60be4f5c76938ed Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Tue, 18 Aug 2026 07:48:26 +0900 Subject: [PATCH] engine: a mode mark does not outlive its owner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mark a mode switch leaves, from 6a6749e, is cleared at the top of transition(), so the next key takes it away. Nothing takes it away when there is no next key. reset() clears the pending text, the raw state, the okurigana split and the search, and leaves modemark alone -- so a release or a lifecycle reset flushes everything the popup was showing except the one thing that is still drawn. Then samedraw() sees a picture identical to the last one and redraw() sends nothing, and win.c only ever unmaps from a Drawcmd (winshow, the sole caller of both map and unmap). So the popup stays. Press the 한/영 key, type nothing, and alt-tab: 한 sits above every window, override redirect and typed as a tooltip, until the next keystroke in any strans-aware field -- which may be minutes. Counted on a private X server, viewable override-redirect windows: before after at rest 0 0 after Ctrl+S 1 1 after letting go 1 0 reset() is where it belongs rather than the release arm, because the mark is one more thing that is pending: it is drawn only when nothing else is (snapshot), and it means the switch has not been typed on yet. Clearing it there is safe for the switch itself only because transition() zeroes it at the top and setlang() runs after flush() -- reset() is reached through that flush, before the new mark is set. That ordering is not obvious, and it is exactly what engine/direct-language-modes already asserts at engine_test.c:584; breaking setlang so a switch marks nothing fails that line and the new ones together. Not a Wayland defect: wl.c's leave() calls hidepopup() unconditionally after the release, so its popup was already coming down. X11 was the frontend that trusted the picture. The new case goes in engine/direct-language-modes beside the mark assertions that were already there, and drives imhandlekey rather than transition, since the defect is on the owner-release path. Without the fix: engine_test.c:619: check failed: draindraw(&dc) > 0 that is, the engine published no picture at all. Its second line needs its own reason to exist, so it was checked by removing the fix and making samedraw() answer 0 for everything: the picture then arrives and still carries the mark, engine_test.c:620: want 0, got 1 (dc.pre.n) which is the assertion that would otherwise have been decoration. The two setup lines are load-bearing too -- with setlang marking nothing they fail rather than passing vacuously, since an empty picture equal to the last one is never sent. 90 unit, check-live, check-stress and valgrind clean; the count is unchanged because the case joined a test that already existed. Co-Authored-By: Claude Opus 5 (1M context) --- strans.c | 1 + tests/engine_test.c | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/strans.c b/strans.c index ad2b4c6..394d7c1 100644 --- a/strans.c +++ b/strans.c @@ -673,6 +673,7 @@ reset(void) { sclear(&im.pre); sclear(&im.raw); + modemark = 0; okuriat = -1; endsearch(); } diff --git a/tests/engine_test.c b/tests/engine_test.c index 662176e..0f885b4 100644 --- a/tests/engine_test.c +++ b/tests/engine_test.c @@ -567,6 +567,7 @@ engine_direct_language_modes(struct ct *t) { Drawcmd dc; Str com; + char owner; init(); draindraw(nil); @@ -607,6 +608,16 @@ engine_direct_language_modes(struct ct *t) checkstr(t, "Korean without map", "ㄱ", &im.pre); CT_CHECK(t, keystroke(Kback, 0, &com)); CT_EQ_INT(t, 0, im.pre.n); + + /* Leaving takes the mark away as surely as typing does. */ + init(); + draindraw(nil); + ownerrequest(&owner, Keypress, 's', Mctrl); + if(CT_CHECK(t, draindraw(&dc) > 0)) + checkstr(t, "mode mark", "한", &dc.pre); + ownerrequest(&owner, Keyrelease, 0, 0); + if(CT_CHECK(t, draindraw(&dc) > 0)) + CT_EQ_INT(t, 0, dc.pre.n); } void