Skip to content

fix: position host cursor for CJK IME even when app hides cursor - #4951

Merged
imsnif merged 4 commits into
zellij-org:mainfrom
satoruhiga:fix/ime-cursor-positioning
Apr 3, 2026
Merged

imsnif merged 4 commits into
zellij-org:mainfrom
satoruhiga:fix/ime-cursor-positioning

Conversation

@satoruhiga

@satoruhiga satoruhiga commented Mar 30, 2026

Copy link
Copy Markdown
Contributor

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, then
show it again and reposition it after rendering. Zellij's render_cursor()
treats a hidden cursor as "no cursor position to report" — it sends
ESC[?25l to the host terminal but does not send a CUP sequence to
position 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 None branch of render_cursor() (cursor hidden by app), still send a
CUP 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:

  • Add cursor_position_for_ime() to Grid and Pane trait — returns cursor
    position ignoring cursor_is_hidden
  • Add is_cursor_not_occluded() to Output — floating pane visibility check
    without the side effect of cursor_is_visible() (which writes to
    cursor_coordinates and would corrupt the crop check in serialize())
  • Add ime_cursor_positions cache to Tab — change detection to avoid
    sending CUP every frame
  • Send hide-cursor before CUP to prevent momentary cursor flash

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:

  • Claude Code (v2.1.93) with macOS default Japanese IME and macSKK
  • nvim — no regression
  • zsh — no regression

Related issues


This change was investigated and implemented with the help of Claude Code.

@satoruhiga

Copy link
Copy Markdown
Contributor Author

Confirmed working on Windows as well.

Tested with Windows 11 + Windows Terminal + Zellij (this branch) + Claude Code.
The IME cursor positioning issue was also occurring on Windows, and this fix resolves it.

@JiehoonKwak

Copy link
Copy Markdown

I also tested in macOS 26.4 with ghostty, and it works great for me! Hope this PR to me merged soon

@imsnif

imsnif commented Mar 31, 2026

Copy link
Copy Markdown
Member

@tlinford - will this be solved by grapheme segmentation work, or is it unrelated?

@tlinford

Copy link
Copy Markdown
Member

@tlinford - will this be solved by grapheme segmentation work, or is it unrelated?

this is a different issue

@imsnif

imsnif commented Apr 2, 2026

Copy link
Copy Markdown
Member

Hey @satoruhiga - thanks for this. I'd be happy to merge this, but I'd like to ask for some changes:

  1. While I don't mind code written by LLMs (with the usual "as long as it's not copyrighted" caveat), Claude Code is a commercial product. I don't want to give a commercial company free ad-space in our commit logs. So please remove the commits from Claude Code and change them to be under your name. Thanks for understanding.
  2. Regarding the technical aspect: it seems here that we're going through a second path during render in the case where we receive None from an occluded cursor. Instead of doing that, could we change the functions returning cursor coordinates to return them unconditionally (assuming the cursor is in the pane, that is) and then also return a bool of whether the cursor is shown or hidden?

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.
@satoruhiga

Copy link
Copy Markdown
Contributor Author

@imsnif Thanks for the review!

  1. Removed the Co-Authored-By line.

  2. Refactored as suggested: cursor_coordinates() now returns the position
    unconditionally (within bounds) along with a bool for visibility.
    render_cursor() uses this to always position the host cursor via CUP,
    even when the app has hidden it.

    All the IME-specific code from the previous revision
    (cursor_position_for_ime, is_cursor_not_occluded, ime_cursor_positions)
    has been removed — the logic is now integrated into the existing path.

    Test files are updated to match the new return type. Existing test behavior
    is preserved (snapshots and plugin API only use coordinates when cursor is
    visible).

Tested on macOS — IME still works correctly. cargo test -p zellij-server
passes (938 tests).

@imsnif

imsnif commented Apr 3, 2026

Copy link
Copy Markdown
Member

Thanks for the changes! I added a test and ran rustfmt. Once CI passes I'll merge.

@imsnif
imsnif merged commit 27a825e into zellij-org:main Apr 3, 2026
9 checks passed
@satoruhiga

Copy link
Copy Markdown
Contributor Author

Thank you for the quick review and merge, @imsnif!

Zellij is an amazing piece of software — this fix will make a lot of
CJK developers very happy. Appreciate all the work you and the team
put into it.

@satoruhiga
satoruhiga deleted the fix/ime-cursor-positioning branch April 4, 2026 16:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants