-
-
Notifications
You must be signed in to change notification settings - Fork 846
Add Synchronized Update Mode support to fix IME issues #846
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Synchronized Update Mode support to fix IME issues #846
Conversation
|
This is a good idea. Thanks for working on it. |
|
CI is failing. |
|
AI review: 1) It prints escape sequences in CI and other non-TTY contextsInk currently has explicit “CI mode” branches to avoid ANSI tricks. This PR adds That means CI logs/snapshots (and any redirected output) can now contain raw Fix: gate synchronized-update emission behind 2) It writes synchronized-update sequences to
|
- Strip synchronized update control characters in test stdout helper - Remove async from ErrorBoundary render method in tests to fix React 19 compatibility - Fix log-update tests to strip control characters - Enable colors in wrap-ansi test to ensure correct trimming behavior
|
This commit: 828f891 may be an improvement. Can you give it a try? |
|
Yes, it's an improvement, but it still requires more work for it to be in a mergable shape. |
- Update createStdout helper to support skipping empty outputs (control characters only) - Use skipEmpty option in renderToString to handle Static component output behavior - Strip control characters in measure-element tests - Fix lint errors
|
I commited 5459615 . I hope ci doesn't fail :) |
…Mode" This reverts commit 5459615.
- Handle exit code 13 from Node.js top-level await warnings - Filter Node.js warning messages from test output assertions - Strip Synchronized Update Mode sequences in measure-element test - Use flexible assertion for dim+bold ANSI code order in text test - Update focus tests to use notThrows() instead of empty output check
|
Please wait a moment, I'll test it with a small sample app and let you know. |
…-for-ime-test-with-sample-code Fix/add synchronized update mode for ime test with sample code
…-for-ime-with-zellij Fix/add synchronized update mode for ime with zellij
OverviewI tested example code (examples/ime-input/index.ts) with alacritty (mac and windows wsl), wezterm(mac), ghostty(mac), terminal(mac) & zellij, tmux. This PR adds IME (Input Method Editor) support. TestsKey Features
Usage Exampleimport React, { useState, useLayoutEffect } from "react";
import stringWidth from "string-width";
import { render, useInput, useApp, useCursor, Box, Text } from "ink";
function ImeInputDemo() {
const { exit } = useApp();
const { setCursorPosition } = useCursor();
const [inputText, setInputText] = useState("");
// Set cursor position for IME candidate window
useLayoutEffect(() => {
// Calculate cursor position based on your layout
// x: column position (0-based)
// y: row from bottom (1 = last line, 2 = second to last, etc.)
const prefixWidth = 5; // Adjust based on your layout (borders, padding, prompt)
const textWidth = stringWidth(inputText);
const cursorX = prefixWidth + textWidth;
const cursorY = 2; // Line where input text is displayed
setCursorPosition({
x: cursorX,
y: cursorY,
visible: true, // Show cursor for IME support
});
return () => {
setCursorPosition(undefined);
};
}, [inputText, setCursorPosition]);
useInput((input, key) => {
if (key.escape || (key.ctrl && input === "c")) {
exit();
return;
}
if (key.return) {
// Handle submit
setInputText("");
return;
}
if (key.backspace || key.delete) {
setInputText((prev) => prev.slice(0, -1));
return;
}
if (input && !key.ctrl && !key.meta) {
setInputText((prev) => prev + input);
}
});
return (
<Box flexDirection="column" paddingX={1}>
<Box borderStyle="round" borderColor="yellow" paddingX={1}>
<Text color="yellow">{"> "}</Text>
<Text>{inputText}</Text>
</Box>
</Box>
);
}
render(<ImeInputDemo />);Important NotesManual cursor position calculation is required. The current implementation provides low-level APIs for IME support. Application developers need to:
This approach is flexible but requires careful calculation based on your specific layout. Future Work: ink-text-input IntegrationTo provide a better developer experience, the ink-text-input package should be updated to use these APIs internally. This would allow developers to simply use I will create A PR to ink-text-input after this PR is merged. Tested EnvironmentsThis implementation has been tested on the following environments: macOS
Windows (WSL)
All environments confirmed working with Japanese IME input. |
|
I tackled the same problem with a different approach in #866 — BSU/ESU is emitted once per render cycle (not per individual write) with TTY/CI guard, and cursor logic is split into pure helper functions. Happy to collaborate on whichever direction is preferred. |
- Merged upstream/master changes including: - App component refactored from class to function component - ErrorBoundary component for error handling - Concurrent mode support with Suspense - Various test improvements - Added TTY guard for synchronized update mode (BSU/ESU): - Only emit escape sequences on TTY streams - Disabled in CI environments - Addresses reviewer feedback on PR vadimdemedes#846 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
@juniqlim Thanks for your work on #866! I've just updated this PR with some improvements based on the reviewer feedback:
Your approach of emitting BSU/ESU once per render cycle is cleaner than per-write. I'm happy to collaborate - we could potentially merge the best parts of both PRs. Would you be open to reviewing this updated implementation, or should we consolidate efforts into one PR? |
|
@sindresorhus I've addressed the feedback.
The main remaining difference from the ideal approach (as noted in your review) is that BSU/ESU is still emitted per-write rather than once per render cycle. @juniqlim's #866 takes that approach - happy to collaborate on consolidating if preferred. Please re-run CI when you have a chance. Thanks! |
|
Going to close this, but I'm happy to consider follow up improvements to #866 |

Summary
This PR implements Synchronized Update Mode (CSI ? 2026) and cursor positioning APIs to fix IME (Input Method Editor) rendering issues affecting users of Chinese, Japanese, and Korean languages in terminal multiplexers.
Problem
When using Ink applications with IME input in terminal multiplexers like Zellij and tmux, users experience:
Root Cause
React Ink does not send Synchronized Update Mode escape sequences, which prevents terminal multiplexers from detecting proper frame boundaries. This causes inaccurate cursor position reporting to the terminal emulator, resulting in IME candidate windows appearing at incorrect positions.
Solution
1. Synchronized Update Mode with TTY Guard
Wraps terminal output with Synchronized Update Mode escape sequences:
\^[[?2026h- Begin Synchronized Update (BSU)\^[[?2026l- End Synchronized Update (ESU)TTY Guard: Only emits escape sequences when:
stream.isTTY === true(real TTY, not piped output)This addresses the reviewer feedback about CI/non-TTY environments.
2.
useCursorHookNew hook for controlling cursor position after rendering, essential for IME support:
Modified Files
src/log-update.ts
shouldSynchronize()helper with TTY/CI guardrender()in bothcreateStandard()andcreateIncremental()functionssrc/ink.tsx
onRender()methodhandleCursorPositionChange()for cursor position updatessrc/components/App.tsx
CursorContextprovider foruseCursorhooksrc/hooks/use-cursor.ts & src/components/CursorContext.tsx
useCursorhook implementationBenefits
Supported Terminals
Modern terminals that support Synchronized Update Mode:
Legacy terminals simply ignore these sequences, maintaining backward compatibility.
Testing
Tested environments:
Usage Example
Future Work
After this PR is merged, I plan to create a PR to ink-text-input to integrate these APIs, providing automatic IME support without manual cursor calculation.
References