fix(ui_type): use clipboard-based approach for non-US keyboard layouts#44
fix(ui_type): use clipboard-based approach for non-US keyboard layouts#44davidlandais wants to merge 2 commits into
Conversation
Fixes joshuayoes#43 The previous implementation used `idb ui text` which sends keystrokes assuming a US QWERTY keyboard layout. This caused incorrect characters to be typed on non-US layouts (AZERTY, QWERTZ, etc.). For example, typing "mobiletest@example.com" on a French AZERTY keyboard would result in ",obiletestàexa,ple:co," because: - 'm' key position → ',' on AZERTY - '@' (Shift+2) → different character This fix uses a clipboard-based approach: 1. Copy text to simulator clipboard via `xcrun simctl pbcopy` 2. Trigger paste with AppleScript (Cmd+V) 3. Auto-tap the "Paste" confirmation button (iOS 16+ security feature) The paste button detection supports multiple locales: - English: "Paste" - French: "Coller" - German: "Einsetzen" - Spanish: "Pegar" - Italian: "Incolla", "Inserisci" - Dutch: "Plakken" - Portuguese: "Colar"
|
Hi! 👋 I want to be transparent about this contribution. I'm not an iOS/Android development expert – I'm primarily a user who encountered this keyboard layout issue while using Claude (Opus 4.5) for my projects. When I hit this bug, I asked the AI to analyze the problem and propose a fix. I cloned your repository, let Claude work on it, tested the solution on my setup (French AZERTY keyboard + iOS Simulator), and it worked for my use case. Important caveats:
I'm absolutely not questioning your expertise or suggesting this is production-ready code. This PR is meant as a starting point or inspiration that might help accelerate your own implementation if you choose to address this issue. Feel free to:
Thank you for maintaining this project. <3 Lovely from France. |
|
Hey, thanks for the pull request and the thoughtful comment! This approach adds a lot more complexity to their fairly simple command that I would like, but I also would like to make sure it's usable for non-US users. Ideally I'd like to fix it upstream in idb at facebook/idb#666, but I'm not sure they are accepting external contributions at the moment. I will need to some thinking on how I would like to address this gap. A few options off the top of my head:
I'm pretty swamped with some other professional projects and demands in my personal life at the moment, so I can't guarantee I will get to this quickly, but I would open to other community effort in this direction in the mean time. |
|
I got the chance to think about it a little bit more this weekend and with the help of Claude, I came up with an idea to keep the tool the same for simple use cases but include support for your approach for special characters and non US keyboards. // Add near the top with other constants
const SAFE_TEXT_PATTERN = /^[a-zA-Z0-9 ]*$/;
const PASTE_BUTTON_LABELS = ["Paste", "Coller", "Einsetzen", "Pegar", "Incolla", "Plakken", "Colar"];
// Helper to find element in accessibility tree
function findElementByLabels(elements: any[], labels: string[]): any | null {
for (const el of elements) {
if (labels.includes(el.AXLabel)) return el;
if (el.children) {
const found = findElementByLabels(el.children, labels);
if (found) return found;
}
}
return null;
}
server.tool(
"ui_type",
"Input text into the iOS Simulator",
{
udid: z
.string()
.regex(UDID_REGEX)
.optional()
.describe("Udid of target, can also be set with the IDB_UDID env var"),
text: z
.string()
.max(500)
.regex(/^[\x20-\x7E]+$/)
.describe("Text to input"),
},
{ title: "UI Type", readOnlyHint: false, openWorldHint: true },
async ({ udid, text }) => {
try {
const actualUdid = await getBootedDeviceId(udid);
// Fast path: simple alphanumeric text uses idb directly
if (SAFE_TEXT_PATTERN.test(text)) {
const { stderr } = await idb("ui", "text", "--udid", actualUdid, "--", text);
if (stderr) throw new Error(stderr);
return {
isError: false,
content: [{ type: "text", text: "Typed successfully" }],
};
}
// Clipboard path: handles @, #, special chars, non-US keyboards
// 1. Copy to simulator clipboard via simctl
await run("xcrun", ["simctl", "pbpaste", actualUdid, text]);
// Note: simctl pbpaste reads from stdin, so we need spawn instead:
await new Promise<void>((resolve, reject) => {
const proc = spawn("xcrun", ["simctl", "pbcopy", actualUdid]);
proc.stdin.write(text);
proc.stdin.end();
proc.on("close", (code) => (code === 0 ? resolve() : reject(new Error(`pbcopy failed with code ${code}`))));
proc.on("error", reject);
});
// 2. Trigger Cmd+V (keycode 9 = 'v', modifier 1 = command)
await idb("ui", "key", "--udid", actualUdid, "9", "--modifier", "1");
// 3. Handle iOS 16+ paste confirmation with single describe-all call
await new Promise((resolve) => setTimeout(resolve, 300));
const { stdout } = await idb("ui", "describe-all", "--udid", actualUdid, "--json");
const elements = JSON.parse(stdout);
const pasteButton = findElementByLabels(elements, PASTE_BUTTON_LABELS);
if (pasteButton) {
const x = pasteButton.frame.x + pasteButton.frame.width / 2;
const y = pasteButton.frame.y + pasteButton.frame.height / 2;
await idb("ui", "tap", "--udid", actualUdid, String(x), String(y));
}
return {
isError: false,
content: [{ type: "text", text: "Typed successfully" }],
};
} catch (error) {
return {
isError: true,
content: [
{
type: "text",
text: errorWithTroubleshooting(
`Error typing text into the iOS Simulator: ${toError(error).message}`
),
},
],
};
}
}
);If you are willing to clean up the PR a little bit to get closer to this approach and verify that this works for you on your end, I would be willing to test on my end and consider adding it. |
|
I've just read your message. My apologies. I have some time to try your code. I will come back to you. Ty |
…roach Replace the clipboard-based approach (pbcopy + Cmd+V + paste confirmation) with AppleScript `keystroke` which sends Unicode characters directly, making it keyboard-layout-independent without clipboard side effects. Tested on French AZERTY keyboard with iOS 16.4 simulator.
|
Hey @joshuayoes! 👋 Thanks for taking the time to think about this and for your detailed code suggestion! I had Claude (Opus 4.6) work on this under my supervision and validation, and we went through quite a research journey. Testing your suggested approachWe started by implementing your hybrid approach (fast path with 1.
|
| Clipboard (previous) | AppleScript keystroke (new) | |
|---|---|---|
| Lines of code | ~90 | ~8 |
| Layout-independent | ✅ | ✅ |
Special chars (@, #, !) |
✅ | ✅ |
| No iOS 16+ paste popup | ❌ (needs handling) | ✅ |
| Simulates real typing | ❌ (pastes at once) | ✅ (char by char) |
| Requires Simulator focus | ❌ | ✅ |
The only trade-off is that the Simulator must be the frontmost app, which seems acceptable for a local dev tool.
Test results on French AZERTY keyboard (iOS 16.4 Simulator)
| Input | idb ui text (before) |
AppleScript keystroke (after) |
|---|---|---|
amazing jazz |
q,qwing jqww ❌ |
amazing jazz ✅ |
test@example.com |
N/A | test@example.com ✅ |
amazing jazz @Paris! |
N/A | amazing jazz @Paris! ✅ |
The updated PR removes the clipboard approach entirely (-92 lines, +6 lines) and replaces it with a single osascript keystroke call. Build passes with no TypeScript errors.
Let me know what you think! Happy to adjust if needed.

Summary
Fixes #43
This PR fixes the keyboard layout issue where
ui_typesends incorrect characters on non-US keyboard layouts (AZERTY, QWERTZ, etc.).Problem
The previous implementation used
idb ui textwhich sends keystrokes assuming a US QWERTY keyboard layout. For example, typingmobiletest@example.comon a French AZERTY keyboard would result in,obiletestàexa,ple:co,because:mkey position →,on AZERTY@(Shift+2) → different character on AZERTYSolution
This fix uses a clipboard-based approach that is keyboard-layout independent:
xcrun simctl pbcopyto copy text directly to the simulator's clipboardCmd+Vto the Simulator appInternationalization
The paste button detection supports multiple locales:
Test plan
@,.)Breaking changes
None. The function signature and return values remain unchanged.