claude-code-vietnamese-fix 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +124 -0
- package/capture-input.js +24 -0
- package/package.json +24 -0
- package/patch-vietnamese-ime.js +219 -0
package/README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# claude-code-vietnamese-fix
|
|
2
|
+
|
|
3
|
+
> Fix Vietnamese input (Telex/VNI) for Claude Code CLI on Windows — one command, zero config.
|
|
4
|
+
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[]()
|
|
7
|
+
[]()
|
|
8
|
+
|
|
9
|
+
## The Problem
|
|
10
|
+
|
|
11
|
+
Vietnamese users **cannot type** in Claude Code CLI. Characters get duplicated or garbled.
|
|
12
|
+
|
|
13
|
+
**Expected:** `tôi` → **Actual:** `toôooi`
|
|
14
|
+
|
|
15
|
+
Unikey sends backspace (`\x08`) + replacement chars embedded in the input string. Claude Code's input handler doesn't process these embedded BS chars — it inserts the entire string as-is.
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
### Option 1: npx (no install needed)
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx claude-code-vietnamese-fix
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Option 2: Install globally
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install -g claude-code-vietnamese-fix
|
|
29
|
+
claude-code-vietnamese-fix
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Option 3: Clone and run
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
git clone https://github.com/tvtdev94/claude-code-vietnamese-fix.git
|
|
36
|
+
cd claude-code-vietnamese-fix
|
|
37
|
+
node patch-vietnamese-ime.js
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Then **restart Claude Code**.
|
|
41
|
+
|
|
42
|
+
## How it works
|
|
43
|
+
|
|
44
|
+
The patch wraps Claude Code's `onInput` handler. When embedded BS chars (`\x08`) are detected, it processes each character sequentially against the cursor state:
|
|
45
|
+
- `\x08` (BS) → `cursor.backspace()` — deletes the previous char
|
|
46
|
+
- Any other char → `cursor.insert(char)` — inserts normally
|
|
47
|
+
|
|
48
|
+
The final cursor state is applied atomically.
|
|
49
|
+
|
|
50
|
+
## Commands
|
|
51
|
+
|
|
52
|
+
| Command | Description |
|
|
53
|
+
|---------|-------------|
|
|
54
|
+
| `npx claude-code-vietnamese-fix` | Patch cli.js (auto-finds, auto-backups) |
|
|
55
|
+
| `npx claude-code-vietnamese-fix --status` | Check current patch status |
|
|
56
|
+
| `npx claude-code-vietnamese-fix --restore` | Restore original cli.js from backup |
|
|
57
|
+
| `npx claude-code-vietnamese-fix --silent` | Patch without output if already patched |
|
|
58
|
+
|
|
59
|
+
## Auto-patch After Updates
|
|
60
|
+
|
|
61
|
+
Claude Code updates will overwrite the patch. Add a SessionStart hook to auto-patch:
|
|
62
|
+
|
|
63
|
+
Add to `~/.claude/settings.json`:
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"hooks": {
|
|
68
|
+
"SessionStart": [
|
|
69
|
+
{
|
|
70
|
+
"matcher": "startup|resume|clear|compact",
|
|
71
|
+
"hooks": [
|
|
72
|
+
{
|
|
73
|
+
"type": "command",
|
|
74
|
+
"command": "npx claude-code-vietnamese-fix --silent"
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The script is **idempotent** — safe to run on every session start.
|
|
84
|
+
|
|
85
|
+
## Restore
|
|
86
|
+
|
|
87
|
+
If anything goes wrong:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
npx claude-code-vietnamese-fix --restore
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Backup location: `%APPDATA%\npm\node_modules\@anthropic-ai\claude-code\cli.js.bak`
|
|
94
|
+
|
|
95
|
+
## Compatibility
|
|
96
|
+
|
|
97
|
+
| Item | Status |
|
|
98
|
+
|------|--------|
|
|
99
|
+
| Windows 10/11 | ✅ |
|
|
100
|
+
| Unikey (Telex) | ✅ Tested |
|
|
101
|
+
| Unikey (VNI, VIQR) | ⚠️ Untested (should work) |
|
|
102
|
+
| OpenKey / EVKey | ⚠️ Untested (should work) |
|
|
103
|
+
| npm global install | ✅ |
|
|
104
|
+
| NVM for Windows | ✅ |
|
|
105
|
+
| Claude Code v2.x | ✅ |
|
|
106
|
+
|
|
107
|
+
## Debug
|
|
108
|
+
|
|
109
|
+
Capture raw input bytes to diagnose IME behavior:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
node capture-input.js
|
|
113
|
+
# Type Vietnamese text, press Ctrl+C to stop
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Related
|
|
117
|
+
|
|
118
|
+
- [Issue #3961](https://github.com/anthropics/claude-code/issues/3961) — Unicode Input Handling Fails for Vietnamese Characters
|
|
119
|
+
- [Issue #7989](https://github.com/anthropics/claude-code/issues/7989) — Error typing Vietnamese Telex
|
|
120
|
+
- [Issue #10429](https://github.com/anthropics/claude-code/issues/10429) — Vietnamese Input Not Working
|
|
121
|
+
|
|
122
|
+
## License
|
|
123
|
+
|
|
124
|
+
[MIT](LICENSE)
|
package/capture-input.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Capture raw stdin bytes to debug Vietnamese IME input.
|
|
4
|
+
* Run: node capture-input.js
|
|
5
|
+
* Type Vietnamese text, press Ctrl+C to stop.
|
|
6
|
+
* Shows hex dump of every byte received.
|
|
7
|
+
*/
|
|
8
|
+
process.stdin.setRawMode(true);
|
|
9
|
+
process.stdin.resume();
|
|
10
|
+
process.stdin.setEncoding(null);
|
|
11
|
+
|
|
12
|
+
console.log("Type Vietnamese text (Ctrl+C to quit):\n");
|
|
13
|
+
|
|
14
|
+
process.stdin.on("data", (buf) => {
|
|
15
|
+
const hex = [...buf].map(b => b.toString(16).padStart(2, "0")).join(" ");
|
|
16
|
+
const chars = [...buf].map(b => b >= 32 && b < 127 ? String.fromCharCode(b) : b === 0x7f ? "<DEL>" : b === 0x08 ? "<BS>" : `\\x${b.toString(16).padStart(2, "0")}`).join("");
|
|
17
|
+
const utf8 = buf.toString("utf8");
|
|
18
|
+
console.log(`HEX: ${hex}`);
|
|
19
|
+
console.log(`RAW: ${chars}`);
|
|
20
|
+
console.log(`UTF: ${utf8}`);
|
|
21
|
+
console.log("---");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
process.on("SIGINT", () => { console.log("\nDone."); process.exit(); });
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-code-vietnamese-fix",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Fix Vietnamese IME input (Unikey/Telex) for Claude Code CLI on Windows",
|
|
5
|
+
"bin": {
|
|
6
|
+
"claude-code-vietnamese-fix": "patch-vietnamese-ime.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"patch": "node patch-vietnamese-ime.js",
|
|
10
|
+
"patch:silent": "node patch-vietnamese-ime.js --silent",
|
|
11
|
+
"status": "node patch-vietnamese-ime.js --status",
|
|
12
|
+
"restore": "node patch-vietnamese-ime.js --restore"
|
|
13
|
+
},
|
|
14
|
+
"keywords": ["claude-code", "vietnamese", "ime", "unikey", "telex", "windows", "patch"],
|
|
15
|
+
"author": "tvtdev94",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/tvtdev94/claude-code-vietnamese-fix"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=16"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Patch Claude Code CLI to fix Vietnamese IME input (Unikey/Telex) on Windows.
|
|
5
|
+
*
|
|
6
|
+
* Root cause: Unikey sends backspace chars (\x08) embedded in the input string
|
|
7
|
+
* along with replacement chars. Claude Code's input handler doesn't process
|
|
8
|
+
* these embedded BS chars — it inserts the entire string as-is, causing
|
|
9
|
+
* duplicate/garbled characters.
|
|
10
|
+
*
|
|
11
|
+
* Fix: Wrap the input handler to detect embedded \x08 (BS) in the input
|
|
12
|
+
* string. When found, process each char sequentially: regular chars get
|
|
13
|
+
* inserted, \x08 triggers a cursor.backspace(). The result is applied
|
|
14
|
+
* atomically to avoid stale-state issues.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* node patch-vietnamese-ime.js # patch (auto-find cli.js)
|
|
18
|
+
* node patch-vietnamese-ime.js --silent # patch, no output if already patched
|
|
19
|
+
* node patch-vietnamese-ime.js --restore # restore from backup
|
|
20
|
+
* node patch-vietnamese-ime.js --status # check patch status
|
|
21
|
+
* node patch-vietnamese-ime.js -f <path> # specify cli.js path manually
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const fs = require("fs");
|
|
25
|
+
const path = require("path");
|
|
26
|
+
const { execSync } = require("child_process");
|
|
27
|
+
|
|
28
|
+
const PATCH_MARKER = "/* _vietnamese_ime_fix_v4_ */";
|
|
29
|
+
|
|
30
|
+
// --- CLI argument parsing ---
|
|
31
|
+
|
|
32
|
+
function parseArgs() {
|
|
33
|
+
const args = process.argv.slice(2);
|
|
34
|
+
const opts = { silent: false, restore: false, status: false, file: null };
|
|
35
|
+
for (let i = 0; i < args.length; i++) {
|
|
36
|
+
if (args[i] === "--silent") opts.silent = true;
|
|
37
|
+
else if (args[i] === "--restore") opts.restore = true;
|
|
38
|
+
else if (args[i] === "--status") opts.status = true;
|
|
39
|
+
else if (args[i] === "-f" || args[i] === "--file") opts.file = args[++i];
|
|
40
|
+
}
|
|
41
|
+
return opts;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// --- Locate cli.js on Windows ---
|
|
45
|
+
|
|
46
|
+
function findCliJs() {
|
|
47
|
+
const run = (cmd) => {
|
|
48
|
+
try {
|
|
49
|
+
return execSync(cmd, { stdio: ["ignore", "pipe", "ignore"] })
|
|
50
|
+
.toString().split(/\r?\n/)[0].trim();
|
|
51
|
+
} catch { return ""; }
|
|
52
|
+
};
|
|
53
|
+
const exists = (p) => p && fs.existsSync(p);
|
|
54
|
+
|
|
55
|
+
// npm global
|
|
56
|
+
try {
|
|
57
|
+
const npmRoot = run("npm root -g");
|
|
58
|
+
const p = path.join(npmRoot, "@anthropic-ai", "claude-code", "cli.js");
|
|
59
|
+
if (exists(p)) return p;
|
|
60
|
+
} catch {}
|
|
61
|
+
|
|
62
|
+
// Common Windows paths
|
|
63
|
+
const paths = [
|
|
64
|
+
path.join(process.env.APPDATA || "", "npm", "node_modules", "@anthropic-ai", "claude-code", "cli.js"),
|
|
65
|
+
path.join(process.env.LOCALAPPDATA || "", "npm", "node_modules", "@anthropic-ai", "claude-code", "cli.js"),
|
|
66
|
+
];
|
|
67
|
+
if (process.env.NVM_HOME) {
|
|
68
|
+
try {
|
|
69
|
+
for (const dir of fs.readdirSync(process.env.NVM_HOME)) {
|
|
70
|
+
paths.push(path.join(process.env.NVM_HOME, dir, "node_modules", "@anthropic-ai", "claude-code", "cli.js"));
|
|
71
|
+
}
|
|
72
|
+
} catch {}
|
|
73
|
+
}
|
|
74
|
+
for (const p of paths) { if (exists(p)) return p; }
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// --- Patch logic ---
|
|
79
|
+
|
|
80
|
+
function patchContent(content) {
|
|
81
|
+
if (content.includes(PATCH_MARKER)) {
|
|
82
|
+
return { success: true, alreadyPatched: true };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let patched = content;
|
|
86
|
+
|
|
87
|
+
// Find the input handler function:
|
|
88
|
+
// function n(t,r){let l=G?G(t,r):t;if(l===""&&t!=="")return;...}
|
|
89
|
+
// This is the onInput handler for Claude Code's text input component.
|
|
90
|
+
const outerRe = /function\s+([\w$]+)\(([\w$]+),([\w$]+)\)\{let\s+([\w$]+)=([\w$]+)\?\5\(\2,\3\):\2;if\(\4===""&&\2!==""\)return;/;
|
|
91
|
+
const outerMatch = patched.match(outerRe);
|
|
92
|
+
|
|
93
|
+
if (!outerMatch) {
|
|
94
|
+
return { success: false, message: "Patch failed: input handler function not found" };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const [fullMatch, fn, p1, p2, p3, p4] = outerMatch;
|
|
98
|
+
|
|
99
|
+
// The wrapper intercepts input strings containing \x08 (BS) chars.
|
|
100
|
+
// When detected, it processes each char against the cursor state `h`:
|
|
101
|
+
// - \x08: cursor.backspace()
|
|
102
|
+
// - other: cursor.insert(char)
|
|
103
|
+
// Then applies the final state atomically.
|
|
104
|
+
//
|
|
105
|
+
// For strings without \x08, passes through to original handler unchanged.
|
|
106
|
+
//
|
|
107
|
+
// Variables from the enclosing closure (captured by the original function):
|
|
108
|
+
// h = cursor state, q = setText, L = setOffset
|
|
109
|
+
// These are referenced via the original function's scope.
|
|
110
|
+
const wrapper =
|
|
111
|
+
`${PATCH_MARKER}` +
|
|
112
|
+
`function ${fn}(${p1},${p2}){` +
|
|
113
|
+
// Check for embedded BS chars (0x08) — Vietnamese IME signature
|
|
114
|
+
`if(!${p2}.backspace&&!${p2}.delete&&${p1}.includes("\\b")){` +
|
|
115
|
+
// Process char-by-char against cursor state h (from enclosing closure)
|
|
116
|
+
`var _c=h;` +
|
|
117
|
+
`for(var _i=0;_i<${p1}.length;_i++){` +
|
|
118
|
+
`var _ch=${p1}[_i];` +
|
|
119
|
+
`if(_ch==="\\b"){` +
|
|
120
|
+
`_c=_c.backspace()` +
|
|
121
|
+
`}else{` +
|
|
122
|
+
`_c=_c.insert(_ch)` +
|
|
123
|
+
`}` +
|
|
124
|
+
`}` +
|
|
125
|
+
// Apply final state atomically
|
|
126
|
+
`if(!h.equals(_c)){` +
|
|
127
|
+
`if(h.text!==_c.text)q(_c.text);` +
|
|
128
|
+
`L(_c.offset)` +
|
|
129
|
+
`}` +
|
|
130
|
+
`return` +
|
|
131
|
+
`}` +
|
|
132
|
+
// No embedded BS: pass through to original handler
|
|
133
|
+
`return _imeR(${p1},${p2})}` +
|
|
134
|
+
// Original function renamed to _imeR
|
|
135
|
+
`function _imeR(${p1},${p2}){` +
|
|
136
|
+
`let ${p3}=${p4}?${p4}(${p1},${p2}):${p1};` +
|
|
137
|
+
`if(${p3}===""&&${p1}!=="")return;`;
|
|
138
|
+
|
|
139
|
+
patched = patched.replace(fullMatch, wrapper);
|
|
140
|
+
|
|
141
|
+
if (patched.includes(fullMatch) && !patched.includes("_imeR")) {
|
|
142
|
+
return { success: false, message: "Patch: replacement failed" };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return { success: true, alreadyPatched: false, content: patched };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// --- Backup & restore ---
|
|
149
|
+
|
|
150
|
+
function backupPath(cliPath) { return cliPath + ".bak"; }
|
|
151
|
+
|
|
152
|
+
function createBackup(cliPath) {
|
|
153
|
+
const bak = backupPath(cliPath);
|
|
154
|
+
fs.copyFileSync(cliPath, bak);
|
|
155
|
+
return bak;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function restoreBackup(cliPath) {
|
|
159
|
+
const bak = backupPath(cliPath);
|
|
160
|
+
if (!fs.existsSync(bak)) return { success: false, message: "No backup found at " + bak };
|
|
161
|
+
fs.copyFileSync(bak, cliPath);
|
|
162
|
+
return { success: true };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// --- Main ---
|
|
166
|
+
|
|
167
|
+
function main() {
|
|
168
|
+
const opts = parseArgs();
|
|
169
|
+
const cliPath = opts.file || findCliJs();
|
|
170
|
+
|
|
171
|
+
if (!cliPath || !fs.existsSync(cliPath)) {
|
|
172
|
+
console.error("Error: Could not find Claude Code cli.js");
|
|
173
|
+
if (cliPath) console.error("Tried: " + cliPath);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (opts.status) {
|
|
178
|
+
const content = fs.readFileSync(cliPath, "latin1");
|
|
179
|
+
const patched = content.includes(PATCH_MARKER);
|
|
180
|
+
console.log(`Target: ${cliPath}`);
|
|
181
|
+
console.log(`Status: ${patched ? "PATCHED" : "NOT PATCHED"}`);
|
|
182
|
+
console.log(`Backup: ${fs.existsSync(backupPath(cliPath)) ? "EXISTS" : "NONE"}`);
|
|
183
|
+
process.exit(0);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (opts.restore) {
|
|
187
|
+
const result = restoreBackup(cliPath);
|
|
188
|
+
if (!result.success) { console.error(result.message); process.exit(1); }
|
|
189
|
+
console.log("Restored cli.js from backup");
|
|
190
|
+
process.exit(0);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Restore from backup first if exists (ensure clean base for patching)
|
|
194
|
+
if (fs.existsSync(backupPath(cliPath))) {
|
|
195
|
+
fs.copyFileSync(backupPath(cliPath), cliPath);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const content = fs.readFileSync(cliPath, "latin1");
|
|
199
|
+
const result = patchContent(content);
|
|
200
|
+
|
|
201
|
+
if (result.alreadyPatched) {
|
|
202
|
+
if (!opts.silent) console.log("Already patched — skipping");
|
|
203
|
+
process.exit(0);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!result.success) {
|
|
207
|
+
console.error(result.message);
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Backup original before writing patch
|
|
212
|
+
createBackup(cliPath);
|
|
213
|
+
if (!opts.silent) console.log("Backup: " + backupPath(cliPath));
|
|
214
|
+
|
|
215
|
+
fs.writeFileSync(cliPath, result.content, "latin1");
|
|
216
|
+
console.log("Patched: " + cliPath);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
main();
|