Skip to content

feat(window): emit native text input events - #1441

Merged
nicolas-abril merged 7 commits into
bendlang:mainfrom
oxura:feat/1019-window-text
Oct 14, 2026
Merged

nicolas-abril merged 7 commits into
bendlang:mainfrom
oxura:feat/1019-window-text

Conversation

@oxura

@oxura oxura commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Closes #1019.

Change

  • Add Text{text: String} to native window events. Key keeps its original canonical codes; text follows key-down when the OS supplies characters.
  • Linux: use a per-window XIM/XIC and Xutf8LookupString, preserving the original event before the legacy key-code lookup changes its modifiers. Focus and input-context resources follow the window lifetime. A missing UTF-8 context returns ENOTSUP instead of silently offering partial text input.
  • macOS: enqueue UTF-8 from NSEvent.characters without lowercasing.
  • Copy text into the existing event queue, not separately allocated C strings; decode mixed fixed/variable-size events in arrival order.
  • Update the exhaustive game proof consumer and document the native text API.

Exercised verification

Actual native Linux window on an isolated X11 display, not mocked events:

  • Shift+A → A; Shift+1 → !; Caps Lock+A → A; Cyrillic Ж and emoji 🙂 retained. Text follows key-down; modifier/release events do not create extra text.
  • Three-byte 你, four-byte 🙂, then 512 ASCII characters injected at zero delay: all 514 text events retained under AddressSanitizer, UndefinedBehaviorSanitizer and leak detection; exit 0, no diagnostics.
  • Rebuilt the unchanged key-only consumer: its original 14 ASCII/modifier Key rows exactly match the pre-change runtime; Cyrillic, Chinese and emoji text is safely dropped under the same sanitizers.
  • Existing headless native window behavior still reports ENOTSUP.
  • Existing App.play program on native and Node: unchanged frame/state/quit output (7, quit, 7).
  • Unchanged repository gate: 54/54.
  • Game proof check before/after reports the same four imported unsafe/foreign definitions (Play.loop, Play.run, Main.mode, main); no whole-proof pass is claimed.

Limits

macOS code was not built or exercised on this Linux workstation. This does not implement full IME editing (#1022). JavaScript native windows remain unsupported as before.

Release-note policy

Removed my unreleased changelog entry following maintainer review. CHANGELOG.md is now unchanged from main; implementation and previously exercised verification are unchanged.

oxura and others added 5 commits October 12, 2026 19:43
…d fail

macOS: BendView is an NSTextInputClient; keyDown goes through
interpretKeyEvents, and insertText sends Text, so input methods and dead
keys reach it and arrows, Enter and Backspace stay Keys.
Linux: open the input method under the user's locale (XMODIFIERS, else
Xlib's own); without one, Text comes from the key's symbol instead of
Window.open failing. A key the method takes is still one Key.
Text rides in the existing five-word event: up to 16 UTF-8 bytes, cut
between characters, control characters dropped.
@nicolas-abril

Copy link
Copy Markdown
Collaborator

Rebased on main and pushed follow-up commits (255d3a9, 5f71867, 7c8faee). Still not working: the macOS input method is incomplete.

What changed:

  • Text events are fixed-size (≤16 UTF-8 bytes); longer commits are split. Control characters are dropped. Key is still sent for every press.
  • macOS: BendView implements NSTextInputClient (insertText → Text).
  • Linux: XIM through the locale, using C.UTF-8 when the codeset isn't UTF-8. If XIM is missing it falls back to XLookupString, so Window.open no longer fails with ENOTSUP. The event queue is unchanged.
  • GUIDE.md is left as it was because it is at its 8000-token cap. base.bend documents Text.

Tested:

  • Gates on macOS: repo 54/54, ping 55/55, test 1606/1606, perf 124/124.
  • Linux test suite: 1605/1606. The one failure, io_tcp_send_slow_peer, timed out under load and passed 3/3 reruns.
  • Linux typing with injected X11 keys, in UTF-8, C, pt_BR, @im=ibus and no-locale setups: a A ! é ã, Cyrillic, 你 and emoji arrive as Text. Return, BackSpace, Tab and Ctrl-a arrive as Key only. Without an input method, é falls back to e.
  • macOS U.S. layout works: a A ! é ñ and space arrive as Text, Return/Delete/Tab arrive as Key only, and Ctrl/Cmd+a produce no Text.

Still broken on macOS:

  • Korean 2-Set: gks comes out as separate jamo (ㅎ ㅏ) instead of 한. 안 and 녕 commit correctly.
  • Japanese Romaji: the candidate window shows 日本, but committing sends no Text.
  • Possible cause (not confirmed): selectedRange returns NSNotFound and there is no real marked/selected range. Next step is to log the IME callbacks and track the selection properly.
  • Compose/set_caret (Window: no input-method (IME) support, so Korean, Japanese and Chinese text cannot be typed #1022) is out of scope here.

Unsupervised AI review.

@oxura

oxura commented Oct 13, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the concrete Korean/Japanese reproductions. I will not claim the macOS IME path fixed from the green gates or the earlier Linux input proof. The available workstation is Linux, with no macOS host or configured SSH target; I cannot exercise the reported NSTextInputClient callbacks here. I also removed the interrupted, unverified marked-text experiment from my local worktree rather than publishing it. The remaining prerequisite is a macOS run capturing setMarkedText/insertText/unmarkText plus selectedRange/markedRange through Korean 2-set composition and Japanese candidate commit. selectedRange = NSNotFound remains a hypothesis, not a confirmed cause. I am keeping #1022 caret/composition work out of this PR and will not push another speculative AppKit patch.

Apple's 2-Set Korean method commits the bare jamo of the first key a
view gets (insertText ㅎ) instead of starting a syllable, so g k s gave
ㅎ ㅏ ㄴ. keyDown now holds the commits of one interpretation; when
the only one is a single compatibility jamo, with no composition before
and no other callback, it interprets the key once more, and the method
composes. Same workaround as winit#4744.

Tested on macOS with real CGEvent keys: Korean g k s / d k s / s k f r
give 한 안 낡; Japanese romaji gives あい and 日本語 on Return; US keys
unchanged.
@nicolas-abril

Copy link
Copy Markdown
Collaborator

Pushed e97225b: fixes the macOS Korean bug.

Cause. Apple's 2-Set Korean input method commits the bare jamo for the first key a view gets (insertText("ㅎ")) instead of starting a syllable. The next keys then start a new syllable without it, so g k s came out as ㅎ ㅏ ㄴ. NSTextView hides this because it can read back and replace text it already holds. A Text event can't be taken back once sent. Upstream bug: winit#4744.

Fix. keyDown holds the commits from one interpretKeyEvents. If all of these are true, the key is interpreted once more and the method composes:

  • the only commit is a single compatibility jamo (U+3131–U+318E)
  • no composition was in progress
  • no other callback ran
  • the key is not a repeat
  • the input source is com.apple.inputmethod.Korean*

Everything else works as before. The change is in the macOS (__OBJC__) code only.

Tested on macOS, with real CGEvent keys going through the system input methods to the /tmp probe app:

  • Korean: g k s → 한, d k s → 안, s k f r → 낡. g k s 5 → 한 then 5. Return commits 한. Backspace inside a syllable works.
  • Japanese romaji: a i Return → あい, nihongo Space Return → 日本語. This already worked on 7c8faee with real keys.
  • US: a b 1 Return Tab Backspace, no change.
  • Gates: repo 54/54, ping 55/55, test 1606/1606, perf 124/124.

Not tested: the Linux laptop. The change doesn't build there.

This comment and commit were written by an AI agent (Unii) working for @nicolas-abril, without human review.

Generalizes the Korean first-jamo replay to any non-keylayout input
source. Vietnamese Telex lost its first composition the same way
(d d i gave "d" + "di" instead of "đi").
@nicolas-abril

Copy link
Copy Markdown
Collaborator

This comment and commit 5bc01f1 were written by an AI agent (Claude), unsupervised. No human has reviewed them.

More macOS input methods tested on 5bc01f1, typing real key events (CGEvent) into a plain NSTextView, a bare NSTextInputClient view, and a Bend App.run that prints Text events:

Input method Keys Bend Text
Simplified Pinyin n i h a o ␣ 你好
Traditional Zhuyin s u 3 c l 3 ↵ 你好
Traditional Cangjie o n f ␣ v n d ␣ 你, 好
Traditional Pinyin z h o n g g u o ␣ 中國
Cantonese Cangjie o n f ␣ v n d ␣ 你, 好
Korean 2-Set / 3-Set g k s ␣ / m f s ␣ 한
Japanese Romaji / Kana n i h o n g o ␣ ↵ / 3 e ↵ 日本語 / あい
Vietnamese Telex d d i ␣ a s ␣ đi, á
Hindi transliteration n a m a s t e ␣ नमस्त, े
US International / ABC dead keys ' e, ⌥e e, ... é, ü, ñ

Switching input methods mid-run (Vietnamese → Korean → Vietnamese) also works.

Fix in 5bc01f1: Vietnamese Telex had the same first-key bug as Korean: d d i gave d then di instead of đi. The replay of the first key now applies to every input method, not just Korean (anything that isn't a plain com.apple.keylayout.*).

Known limits (not changed):

  • A commit longer than 16 UTF-8 bytes splits into several Text events, so the Hindi word arrives as two events.
  • Holding a key repeats it. The macOS press-and-hold accent popup isn't shown.

Gates on this branch merged with main: repo 54/54, ping 57/57, test 1630/1630, perf 124/124 (macOS M4). Linux, CUDA and the cluster weren't run again; this commit only changes the macOS code.

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.

Window: Key events never carry an uppercase letter, so a program cannot tell what the user typed

2 participants