fix: position host cursor for CJK IME even when app hides cursor - #4951
Conversation
|
Confirmed working on Windows as well. Tested with Windows 11 + Windows Terminal + Zellij (this branch) + Claude Code. |
|
I also tested in macOS 26.4 with ghostty, and it works great for me! Hope this PR to me merged soon |
|
@tlinford - will this be solved by grapheme segmentation work, or is it unrelated? |
this is a different issue |
|
Hey @satoruhiga - thanks for this. I'd be happy to merge this, but I'd like to ask for some changes:
Let me know if you have any questions or if I wasn't clear enough. |
When apps like Claude Code (ink/React library) hide the cursor during rendering, Zellij's render_cursor() was not sending a CUP sequence to the host terminal. This caused IME composition candidates to appear at the wrong position, breaking CJK input in these apps. Change cursor_coordinates() to return the cursor position unconditionally (as long as it is within bounds) along with a bool indicating visibility. render_cursor() now positions the host cursor even when the app has hidden it, so the IME subsystem can read the correct cursor position.
|
@imsnif Thanks for the review!
Tested on macOS — IME still works correctly. |
|
Thanks for the changes! I added a test and ran |
|
Thank you for the quick review and merge, @imsnif! Zellij is an amazing piece of software — this fix will make a lot of |
Summary
When apps like Claude Code (which uses the ink/React library) run inside Zellij,
IME composition candidates appear at the wrong position. This makes CJK input
(Japanese, Chinese, Korean) effectively broken in these apps.
The same apps work correctly in a bare terminal or under tmux.
Root cause
ink-based TUIs hide the cursor (
CSI ? 25 l) during each render cycle, thenshow it again and reposition it after rendering. Zellij's
render_cursor()treats a hidden cursor as "no cursor position to report" — it sends
ESC[?25lto the host terminal but does not send a CUP sequence toposition it. After a content render (which moves the cursor around to draw
character chunks), the host terminal's cursor ends up at an arbitrary position.
The host terminal's IME system reads the cursor position to place the
composition window, so it appears in the wrong place.
Fix
In the
Nonebranch ofrender_cursor()(cursor hidden by app), still send aCUP escape sequence to position the host terminal cursor at the correct
location, then hide it. The IME subsystem reads the cursor position regardless
of visibility.
Changes:
cursor_position_for_ime()toGridandPanetrait — returns cursorposition ignoring
cursor_is_hiddenis_cursor_not_occluded()toOutput— floating pane visibility checkwithout the side effect of
cursor_is_visible()(which writes tocursor_coordinatesand would corrupt the crop check inserialize())ime_cursor_positionscache toTab— change detection to avoidsending CUP every frame
Testing
Screen.Recording.0008-03-30.at.16.29.32.mov
Tested on macOS (Apple Silicon, Darwin 25.3.0) with Ghostty + Zellij.
IME input confirmed working:
Related issues
report Zellij-specific breakage in comments)
This change was investigated and implemented with the help of Claude Code.