This commit is contained in:
2026-02-19 00:33:08 -08:00
parent e37f3dd7b1
commit 70dd0779f2
143 changed files with 31888 additions and 0 deletions

View File

@@ -0,0 +1,391 @@
#!/usr/bin/env python3
"""
iOS Keyboard Controller - Text Entry and Hardware Buttons
Handles keyboard input, special keys, and hardware button simulation.
Token-efficient text entry and navigation control.
This script provides text input and hardware button control for iOS simulator
automation. It handles both typing text strings and pressing special keys like
return, delete, tab, etc. Also controls hardware buttons like home and lock.
Key Features:
- Type text strings into focused elements
- Press special keys (return, delete, tab, space, arrows)
- Hardware button simulation (home, lock, volume, screenshot)
- Character-by-character typing with delays (for animations)
- Multiple key press support
- iOS HID key code mapping for reliability
Usage Examples:
# Type text into focused field
python scripts/keyboard.py --type "hello@example.com" --udid <device-id>
# Press return key to submit
python scripts/keyboard.py --key return --udid <device-id>
# Press delete 3 times
python scripts/keyboard.py --key delete --key delete --key delete --udid <device-id>
# Press home button
python scripts/keyboard.py --button home --udid <device-id>
# Press lock button
python scripts/keyboard.py --button lock --udid <device-id>
# Type with delay between characters (for animations)
python scripts/keyboard.py --type "slow typing" --delay 0.1 --udid <device-id>
Output Format:
Typed: "hello@example.com"
Pressed return
Pressed home button
Special Keys Supported:
- return/enter: Submit forms, new lines (HID code 40)
- delete/backspace: Remove characters (HID code 42)
- tab: Navigate between fields (HID code 43)
- space: Space character (HID code 44)
- escape: Cancel/dismiss (HID code 41)
- up/down/left/right: Arrow keys (HID codes 82/81/80/79)
Hardware Buttons Supported:
- home: Return to home screen
- lock/power: Lock device
- volume-up/volume-down: Volume control
- ringer: Toggle mute
- screenshot: Capture screen
Technical Details:
- Uses `idb ui text` for typing text strings
- Uses `idb ui key <code>` for special keys with iOS HID codes
- HID codes from Apple's UIKeyboardHIDUsage specification
- Hardware buttons use `xcrun simctl` button actions
- Text entry works on currently focused element
- Special keys are integers (40=Return, 42=Delete, etc.)
"""
import argparse
import subprocess
import sys
import time
from common import resolve_udid
class KeyboardController:
"""Controls keyboard and hardware buttons on iOS simulator."""
# Special key mappings to iOS HID key codes
# See: https://developer.apple.com/documentation/uikit/uikeyboardhidusage
SPECIAL_KEYS = {
"return": 40,
"enter": 40,
"delete": 42,
"backspace": 42,
"tab": 43,
"space": 44,
"escape": 41,
"up": 82,
"down": 81,
"left": 80,
"right": 79,
}
# Hardware button mappings
HARDWARE_BUTTONS = {
"home": "HOME",
"lock": "LOCK",
"volume-up": "VOLUME_UP",
"volume-down": "VOLUME_DOWN",
"ringer": "RINGER",
"power": "LOCK", # Alias
"screenshot": "SCREENSHOT",
}
def __init__(self, udid: str | None = None):
"""Initialize keyboard controller."""
self.udid = udid
def type_text(self, text: str, delay: float = 0.0) -> bool:
"""
Type text into current focus.
Args:
text: Text to type
delay: Delay between characters (for slow typing effect)
Returns:
Success status
"""
if delay > 0:
# Type character by character with delay
for char in text:
if not self._type_single(char):
return False
time.sleep(delay)
return True
# Type all at once (efficient)
return self._type_single(text)
def _type_single(self, text: str) -> bool:
"""Type text using IDB."""
cmd = ["idb", "ui", "text", text]
if self.udid:
cmd.extend(["--udid", self.udid])
try:
subprocess.run(cmd, capture_output=True, check=True)
return True
except subprocess.CalledProcessError:
return False
def press_key(self, key: str, count: int = 1) -> bool:
"""
Press a special key.
Args:
key: Key name (return, delete, tab, etc.)
count: Number of times to press
Returns:
Success status
"""
# Map key name to IDB key code
key_code = self.SPECIAL_KEYS.get(key.lower())
if not key_code:
# Try as literal integer key code
try:
key_code = int(key)
except ValueError:
return False
cmd = ["idb", "ui", "key", str(key_code)]
if self.udid:
cmd.extend(["--udid", self.udid])
try:
for _ in range(count):
subprocess.run(cmd, capture_output=True, check=True)
if count > 1:
time.sleep(0.1) # Small delay for multiple presses
return True
except subprocess.CalledProcessError:
return False
def press_key_sequence(self, keys: list[str]) -> bool:
"""
Press a sequence of keys.
Args:
keys: List of key names
Returns:
Success status
"""
cmd_base = ["idb", "ui", "key-sequence"]
# Map keys to codes
mapped_keys = []
for key in keys:
mapped = self.SPECIAL_KEYS.get(key.lower())
if mapped is None:
# Try as integer
try:
mapped = int(key)
except ValueError:
return False
mapped_keys.append(str(mapped))
cmd = cmd_base + mapped_keys
if self.udid:
cmd.extend(["--udid", self.udid])
try:
subprocess.run(cmd, capture_output=True, check=True)
return True
except subprocess.CalledProcessError:
return False
def press_hardware_button(self, button: str) -> bool:
"""
Press hardware button.
Args:
button: Button name (home, lock, volume-up, etc.)
Returns:
Success status
"""
button_code = self.HARDWARE_BUTTONS.get(button.lower())
if not button_code:
return False
cmd = ["idb", "ui", "button", button_code]
if self.udid:
cmd.extend(["--udid", self.udid])
try:
subprocess.run(cmd, capture_output=True, check=True)
return True
except subprocess.CalledProcessError:
return False
def clear_text(self, select_all: bool = True) -> bool:
"""
Clear text in current field.
Args:
select_all: Use Cmd+A to select all first
Returns:
Success status
"""
if select_all:
# Select all then delete
# Note: This might need adjustment for iOS keyboard shortcuts
success = self.press_key_combo(["cmd", "a"])
if success:
return self.press_key("delete")
else:
# Just delete multiple times
return self.press_key("delete", count=50)
return None
def press_key_combo(self, keys: list[str]) -> bool:
"""
Press key combination (like Cmd+A).
Args:
keys: List of keys to press together
Returns:
Success status
"""
# IDB doesn't directly support key combos
# This is a workaround - may need platform-specific handling
if "cmd" in keys or "command" in keys:
# Handle common shortcuts
if "a" in keys:
# Select all - might work with key sequence
return self.press_key_sequence(["command", "a"])
if "c" in keys:
return self.press_key_sequence(["command", "c"])
if "v" in keys:
return self.press_key_sequence(["command", "v"])
if "x" in keys:
return self.press_key_sequence(["command", "x"])
# Try as sequence
return self.press_key_sequence(keys)
def dismiss_keyboard(self) -> bool:
"""Dismiss on-screen keyboard."""
# Common ways to dismiss keyboard on iOS
# Try Done button first, then Return
success = self.press_key("return")
if not success:
# Try tapping outside (would need coordinate)
pass
return success
def main():
"""Main entry point."""
parser = argparse.ArgumentParser(description="Control keyboard and hardware buttons")
# Text input
parser.add_argument("--type", help="Type text into current focus")
parser.add_argument("--slow", action="store_true", help="Type slowly (character by character)")
# Special keys
parser.add_argument("--key", help="Press special key (return, delete, tab, space, etc.)")
parser.add_argument("--key-sequence", help="Press key sequence (comma-separated)")
parser.add_argument("--count", type=int, default=1, help="Number of times to press key")
# Hardware buttons
parser.add_argument(
"--button",
choices=["home", "lock", "volume-up", "volume-down", "ringer", "screenshot"],
help="Press hardware button",
)
# Other operations
parser.add_argument("--clear", action="store_true", help="Clear current text field")
parser.add_argument("--dismiss", action="store_true", help="Dismiss keyboard")
parser.add_argument(
"--udid",
help="Device UDID (auto-detects booted simulator if not provided)",
)
args = parser.parse_args()
# Resolve UDID with auto-detection
try:
udid = resolve_udid(args.udid)
except RuntimeError as e:
print(f"Error: {e}")
sys.exit(1)
controller = KeyboardController(udid=udid)
# Execute requested action
if args.type:
delay = 0.1 if args.slow else 0.0
if controller.type_text(args.type, delay):
if args.slow:
print(f'Typed: "{args.type}" (slowly)')
else:
print(f'Typed: "{args.type}"')
else:
print("Failed to type text")
sys.exit(1)
elif args.key:
if controller.press_key(args.key, args.count):
if args.count > 1:
print(f"Pressed {args.key} ({args.count}x)")
else:
print(f"Pressed {args.key}")
else:
print(f"Failed to press {args.key}")
sys.exit(1)
elif args.key_sequence:
keys = args.key_sequence.split(",")
if controller.press_key_sequence(keys):
print(f"Pressed sequence: {' -> '.join(keys)}")
else:
print("Failed to press key sequence")
sys.exit(1)
elif args.button:
if controller.press_hardware_button(args.button):
print(f"Pressed {args.button} button")
else:
print(f"Failed to press {args.button}")
sys.exit(1)
elif args.clear:
if controller.clear_text():
print("Cleared text field")
else:
print("Failed to clear text")
sys.exit(1)
elif args.dismiss:
if controller.dismiss_keyboard():
print("Dismissed keyboard")
else:
print("Failed to dismiss keyboard")
sys.exit(1)
else:
parser.print_help()
sys.exit(1)
if __name__ == "__main__":
main()