Skip to content

fix(input): route IME text as a key event, keep clipboard clean on paste#90

Open
KoalaHao wants to merge 1 commit into
Norbert515:mainfrom
marsup-space:fix/ime-kitty-associated-text
Open

fix(input): route IME text as a key event, keep clipboard clean on paste#90
KoalaHao wants to merge 1 commit into
Norbert515:mainfrom
marsup-space:fix/ime-kitty-associated-text

Conversation

@KoalaHao

@KoalaHao KoalaHao commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Two related fixes for the IME (Chinese / Japanese / Korean input
method editor) text path on macOS and Linux.

1. Paste path no longer pollutes the system clipboard

On macOS, the Chinese / Japanese / Korean IMEs wrap their
committed candidate text in bracketed-paste markers
(ESC[200~ ... ESC[201~), so a confirmed word like tool or
你好 arrives at the framework as a PasteInputEvent.
Previously TerminalBinding called ClipboardManager.copy on
the payload and then routed a synthetic Ctrl+V so
TextField._paste could read it back from the clipboard. This
worked for real paste, but on the IME path it overwrote
whatever the user had on their system clipboard every single
time they confirmed a candidate.

The framework now stashes the paste text on the binding
(NoctermBinding.setPendingPasteText) and TextField._paste
consumes it via
NoctermBinding.instance.consumePendingPasteText at the top
of its paste path. Real user-initiated Ctrl+V has no pending
text and still reads from the system clipboard as before. The
system clipboard is no longer touched on the IME path.

2. Kitty keyboard protocol: 4-part form (associated text)

When the terminal supports the Kitty keyboard protocol's "report
associated text" flag (iTerm2, WezTerm, Ghostty, Kitty), IME
commits now arrive as ordinary key events with the IME's output
in the character field, instead of being routed through
bracketed-paste mode at all. Format:

CSI codepoint ; modifiers ; base_layout ; text u

where text is percent-encoded UTF-8 (so 你好 arrives as
%E4%BD%A0%E5%A5%BD). The framework now requests this flag at
startup (ESC[>17u — bit 0 disambiguate + bit 4 associated
text) and the input parser recognizes the 4-part form. The
affected _parseKittySequence uses a backward-then-validate scan
to disambiguate the terminator u from u bytes that may
appear un-percent-encoded inside the associated text (u is
unreserved in URL encoding, so e.g. menu is sent verbatim,
not as men%75).

The benefit on supporting terminals is twofold: (a) IME text
reaches the focused widget as a regular KeyboardInputEvent,
not a PasteInputEvent, so it is no longer misrouted through
any app-level paste handling (e.g. file-drop detectors); (b)
the system clipboard is never touched on the IME path (even
the binding-level stash is bypassed). On terminals without
this flag, IME commits still fall through to the bracketed-paste
path and benefit from fix #1.

A small refactor extracts _logicalKeyForCodepoint from
_codepointToKeyEvent so the codepoint → LogicalKey mapping
is shared between the 1–3 part form (character derived from
codepoint) and the 4-part form (character from associated
text, logical key from codepoint).

Tests

  • 4 new tests in text_field_clipboard_test.dart cover the
    pending-paste-text mechanism (IME path, fallthrough to
    clipboard, real Ctrl+V, no-leak to subsequent Ctrl+V).
  • 14 new tests in kitty_associated_text_test.dart cover the
    4-part form (ASCII text, CJK text, percent-encoding,
    sub-parameters, codepoint driving logicalKey, two
    sequences back-to-back, 5-part rejection, mixed
    bracketed-paste + kitty stream, 2-/3-part regression).

Two related fixes for the IME (Chinese / Japanese / Korean input
method editor) text path on macOS and Linux.

On macOS, the Chinese / Japanese / Korean IMEs wrap their
committed candidate text in bracketed-paste markers
(ESC[200~ ... ESC[201~), so a confirmed word like 'tool' or
'你好' arrives at the framework as a PasteInputEvent. Previously
TerminalBinding called ClipboardManager.copy on the payload and
then routed a synthetic Ctrl+V so TextField._paste could read
it back from the clipboard. This worked for real paste, but on
the IME path it overwrote whatever the user had on their
system clipboard every single time they confirmed a candidate.

The framework now stashes the paste text on the binding
(NoctermBinding.setPendingPasteText) and TextField._paste
consumes it via NoctermBinding.instance.consumePendingPasteText
at the top of its paste path. Real user-initiated Ctrl+V has
no pending text and still reads from the system clipboard as
before. The system clipboard is no longer touched on the IME
path.

When the terminal supports the Kitty keyboard protocol's
'report associated text' flag (iTerm2, WezTerm, Ghostty, Kitty),
IME commits now arrive as ordinary key events with the IME's
output in the character field, instead of being routed
through bracketed-paste mode at all. Format:

    CSI codepoint ; modifiers ; base_layout ; text u

where text is percent-encoded UTF-8 (so '你好' arrives as
'%E4%BD%A0%E5%A5%BD'). The framework now requests this flag
at startup (ESC[>17u — bit 0 disambiguate + bit 4 associated
text) and the input parser recognizes the 4-part form. The
affected _parseKittySequence uses a backward-then-validate
scan to disambiguate the terminator 'u' from 'u' bytes that
may appear un-percent-encoded inside the associated text
('u' is unreserved in URL encoding, so e.g. 'menu' is sent
verbatim, not as 'men%75').

The benefit on supporting terminals is twofold: (a) IME text
reaches the focused widget as a regular KeyboardInputEvent,
not a PasteInputEvent, so it is no longer misrouted through
any app-level paste handling (e.g. file-drop detectors);
(b) the system clipboard is never touched on the IME path
(even the binding-level stash is bypassed). On terminals
without this flag, IME commits still fall through to the
bracketed-paste path and benefit from fix #1.

A small refactor extracts _logicalKeyForCodepoint from
_codepointToKeyEvent so the codepoint → LogicalKey mapping
is shared between the 1-3 part form (character derived from
codepoint) and the 4-part form (character from associated
text, logical key from codepoint).

- 4 new tests in text_field_clipboard_test.dart cover the
  pending-paste-text mechanism (IME path, fallthrough to
  clipboard, real Ctrl+V, no-leak to subsequent Ctrl+V).
- 14 new tests in kitty_associated_text_test.dart cover the
  4-part form (ASCII text, CJK text, percent-encoding,
  sub-parameters, codepoint driving logicalKey, two
  sequences back-to-back, 5-part rejection, mixed
  bracketed-paste + kitty stream, 2-/3-part regression).
@KoalaHao
KoalaHao force-pushed the fix/ime-kitty-associated-text branch from 099bdf2 to 4fa5a11 Compare June 24, 2026 03:37
@KoalaHao
KoalaHao marked this pull request as draft June 25, 2026 02:06
KoalaHao added a commit to marsup-space/nocterm that referenced this pull request Jun 25, 2026
PR Norbert515#90 (kitty 4-part form for IME associated text) changed the
kitty sequence parser to scan backward for the 'u' terminator so
the parser can correctly handle 4-part sequences whose associated
text contains raw 'u' bytes. Add regression tests to lock in the
2-/3-part forms (codepoint + modifier) that macOS Cmd+A/C/V/X
rely on — iTerm2 / WezTerm / Ghostty with the kitty keyboard
protocol emit these sequences on every Cmd+A/C/V/X press, and
the new parser must still surface them with meta=true and the
right logicalKey.
@KoalaHao
KoalaHao marked this pull request as ready for review June 25, 2026 06:00
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.

Using opentui

1 participant