Skip to content

Conversation

@takeru
Copy link

@takeru takeru commented Oct 26, 2025

Summary

Implements IME (Input Method Editor) cursor positioning to fix the issue where IME candidate windows appear at the end of output instead of at the actual input position when using CJK input methods.

Problem

When using Ink applications with CJK input methods (Japanese, Chinese, Korean), the IME candidate window appears at the wrong location because the terminal cursor stays at the end of the output instead of the actual input position.

Solution

This PR adds an enableImeCursor option that positions the terminal cursor at the actual input location using:

  1. Cursor marker detection (src/cursor-marker.ts)

    • Detect cursor position marker in rendered output
    • Calculate cursor position in multi-line text with proper CJK character width handling
  2. Cursor positioning (src/log-update.ts)

    • Use relative cursor movement (ANSI A/B/C/D sequences)
    • Cursor save/restore for stable rendering
  3. API addition (src/ink.tsx, src/render.ts, src/components/App.tsx)

    • Add enableImeCursor option (default: false for backward compatibility)
    • Conditionally control cursor visibility

Usage

To enable IME cursor positioning in your Ink application:

1. Enable the option when rendering

import {render} from 'ink';

render(<YourApp />, {
  enableImeCursor: true
});

2. Insert cursor marker at the input position

import {Text} from 'ink';
import {CURSOR_MARKER} from 'ink/cursor-marker';

function TextInput({value, cursorPosition}) {
  const before = value.slice(0, cursorPosition);
  const after = value.slice(cursorPosition);
  
  return (
    <Text>
      {before}
      {CURSOR_MARKER}
      <Text inverse>{value[cursorPosition] || ' '}</Text>
      {after}
    </Text>
  );
}

The CURSOR_MARKER should be placed immediately before the visual cursor position. The terminal cursor will be positioned at this location.

Demo Application

Interactive demo showing IME cursor positioning with multi-line text editing:

npx tsx examples/multiline-edit-demo/multiline-edit-demo.tsx

Key Features

  • ✅ Opt-in feature (backward compatible, default: disabled)
  • ✅ Proper multi-line text handling
  • ✅ Full-width CJK character support (2 cells per character)
  • ✅ Relative cursor movement only (no terminal queries needed)

Test Plan

Run the demo application and verify:

  • Terminal cursor appears at the input position (white █ character)
  • IME candidate window appears at the cursor location
  • Text wrapping and cursor movement work correctly
  • Multi-line editing works as expected

🤖 Generated with Claude Code

takeru and others added 5 commits October 26, 2025 10:19
Add IME (Input Method Editor) cursor positioning support to enable proper
placement of IME candidate windows during CJK text input.

Key features:
- New enableImeCursor option in render() (default: false, opt-in)
- Private Use Area character (U+E000) as invisible cursor marker
- Automatic cursor positioning using ANSI escape sequences
- Cursor save/restore to prevent content jumping
- Measure text without marker for accurate width calculation

When enabled:
- Terminal cursor moves to actual input position
- IME candidate windows appear at correct location
- Cursor visibility controlled by log-update

Implementation:
- src/cursor-marker.ts: Cursor marker constant and detection
- src/ink.tsx: Add enableImeCursor option
- src/log-update.ts: Cursor positioning logic with save/restore
- src/components/App.tsx: Conditional cursor visibility control
- src/render.ts: RenderOptions type extension
- src/measure-text.ts: Strip marker before measuring

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Interactive demo showcasing IME cursor positioning with multi-line text editing.

Features:
- Multi-line text editing with automatic line wrapping at 80 cells
- Full-width character support (CJK = 2 cells, ASCII = 1 cell)
- Arrow key navigation (up/down/left/right)
- Visual cursor display with position information
- Environment variable to toggle IME cursor (ENABLE_IME_CURSOR)
- Bilingual demo text (Japanese with English translation)

Usage:
  # With IME cursor positioning (default)
  npx tsx examples/multiline-edit-demo/multiline-edit-demo.tsx

  # Without IME cursor positioning (for comparison)
  ENABLE_IME_CURSOR=false npx tsx examples/multiline-edit-demo/multiline-edit-demo.tsx

Tests included:
- Standalone test script for individual test cases
- AVA integration tests with node-pty for automated testing
- 15 test cases covering edge cases (empty lines, wrapping, mixed-width chars)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Replace U+E000 (Private Use Area) cursor marker with SGR 999 (undefined
ANSI escape sequence). This eliminates the need for border alignment
workarounds by ensuring the marker is automatically excluded from width
calculations.

Changes:
- cursor-marker.ts: Change CURSOR_MARKER from '\uE000' to '\u001B[999m'
- log-update.ts: Remove 37-line border alignment hack
- measure-text.ts: Remove explicit marker removal (auto-excluded now)
- ink.tsx: Revert unrelated changes

The SGR 999 sequence:
- Is recognized by ansi-tokenize as an ANSI token
- Automatically excluded by widestLine() and stringWidth()
- Does not interfere with existing text styles
- Will never be standardized (999 is out of valid range)

Result: -41 lines, cleaner code, proper fix for border misalignment.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
After early return for !showCursor case, all subsequent code is
guaranteed to have showCursor === true. Remove redundant conditions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@sindresorhus
Copy link
Collaborator

sindresorhus commented Oct 26, 2025

You will have to find some humans to review this code. I don't have time to review vibe coded PRs. Claude Code is not known for producing bug-free code, especially not for complex things like this.

I also recommend getting ChatGPT 5 Thinking (with extended thinking) to review this. It's much smarter.

@takeru
Copy link
Author

takeru commented Oct 26, 2025

Thank you for your reply.
I’ve reviewed it carefully. I don’t fully understand ink, so I can’t say for sure whether this code is perfect, but I’ve gone through all of the main code (though I only skimmed the demo test code).

This is quite a complex change, but it’s an important one for people who need to use an IME. I realize it’s difficult for those who don’t rely on IME to fully understand the importance, so I’ve been thinking for a while about how best to explain it. In the end, I decided to go ahead and open the PR so you could take a look.

Do you think there’s a possibility that this kind of approach could be accepted?
If so, I’d like to continue refining it further.

@takeru takeru marked this pull request as draft October 27, 2025 00:54
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.

2 participants