feat(macOS): support configurable keyboard types for simulated input#2
feat(macOS): support configurable keyboard types for simulated input#2fufesou wants to merge 1 commit into
Conversation
|
@coderabbitai review |
There was a problem hiding this comment.
Pull request overview
This PR adds macOS support for selecting the hardware keyboard type (ANSI/ISO/JIS/Raw/Current) used when translating physical keycodes to characters during simulated keyboard input. It introduces MacKeyboardType and a builder method VirtualInput::with_keyboard_type, plus per-source/per-event keyboard-type wiring through CGEventSourceSetKeyboardType and the KEYBOARD_EVENT_KEYBOARD_TYPE event field. It also adds modifier/dead-key tracking on VirtualInput so that translated Unicode is passed through to the synthesized event, with suppression for Command/Control shortcuts.
Changes:
- New
MacKeyboardTypeenum exported from the crate root and consumed byVirtualInput. simulate.rsrefactored: keyboard event creation extracted into helpers, modifier/dead-key state tracked viaCellfields, andEVENT_SOURCE_USER_DATAnow selected per event kind.keyboard.rsgains a sharedunicode_from_code_with_keyboard_typehelper and reworksKeyboard::modifier_stateto use named bit constants (caps_lock now maps to alphaLock instead of shift).
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
Cargo.toml |
Adds foreign-types dependency for as_ptr() on CGEventSource. |
src/lib.rs |
Exposes new public MacKeyboardType enum (macOS only). |
src/macos/simulate.rs |
Major refactor: keyboard-type plumbing, modifier/dead-key state, helper extraction, new tests. |
src/macos/keyboard.rs |
Extracts unicode_from_code_with_keyboard_type; updates modifier_state to use named bitmask constants (behavior change for caps lock). |
src/codes_conv.rs |
Tiny test fix wrapping code in u32::from(...) to match new types. |
examples/simulate_keyboard_type.rs |
New macOS-only example demonstrating ANSI/ISO/JIS keyboard types. |
Comments suppressed due to low confidence (1)
src/macos/simulate.rs:442
update_modifier_stateis called afterconvert_native_with_sourcehas already produced and posted the event. This means the unicode translation for the current key is performed using the modifier state from before this key was processed. For non-modifier keys this is fine (Shift was set by a prior event), but for the modifier key event itself the state is stale. More importantly, ifcg_event.post()succeeds but produces no event (Nonebranch), the modifier state is not updated, while ifpost()returnsErrlater this is fine since update only happens on success. However, the deeper issue is: if a caller releases a non-modifier key before releasing Shift, the release event goes through with shift still set inmodifier_state, butkeyboard_event_from_keyskips unicode generation forKeyRelease(onlykeydowndoes it), so this is harmless. Consider documenting that modifier state is tracked across calls and is order-sensitive (the caller must press/release modifiers in a sane order).
pub fn simulate(&self, event_type: &EventType) -> Result<(), SimulateError> {
unsafe {
let keyboard_type = keyboard_type_value(self.keyboard_type);
if let Some(cg_event) = convert_native_with_source(
event_type,
&self.source,
keyboard_type,
Some(self.modifier_state()),
Some(&self.dead_state),
) {
cg_event.post(self.tap_loc);
self.update_modifier_state(event_type);
Ok(())
} else {
Err(SimulateError)
}
}
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
1c3283a to
9fff887
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 7 comments.
Comments suppressed due to low confidence (1)
src/macos/simulate.rs:443
- Modifier state is updated only after
cg_event.post(...)succeeds, butupdate_modifier_stateis also skipped entirely whenconvert_native_with_sourcereturnsNone(e.g. an unmapped key, orCGEvent::new_keyboard_eventfailure). For modifier-only presses, this means a failed event-creation silently leaves the tracked modifier state out of sync with what the OS believes the user has held. Consider updating modifier state regardless of whether the synthesized event was successfully posted, or document this behaviour.
unsafe {
let keyboard_type = keyboard_type_value(self.keyboard_type);
if let Some(cg_event) = convert_native_with_source(
event_type,
&self.source,
keyboard_type,
Some(self.modifier_state()),
Some(&self.dead_state),
) {
cg_event.post(self.tap_loc);
self.update_modifier_state(event_type);
Ok(())
} else {
Err(SimulateError)
}
9fff887 to
7fc5898
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (8)
src/macos/simulate.rs:430
update_modifier_statedoes not distinguish left/right variants of a modifier. If both ShiftLeft and ShiftRight are pressed and only one is released,self.shiftis unconditionally cleared on the first release while the other modifier is still physically held. This causes subsequent unicode translation to use a stale modifier state. Same issue applies to Alt/Control/Command. Tracking each side independently (or using a counter) would avoid the inconsistency.
fn update_modifier_state(&self, event_type: &EventType) {
match event_type {
EventType::KeyPress(key) => match modifier_key_from_key(*key) {
Some(ModifierKey::Shift) => self.shift.set(true),
Some(ModifierKey::Alt) => self.alt.set(true),
Some(ModifierKey::CapsLock) => self.caps_lock.set(!self.caps_lock.get()),
Some(ModifierKey::Control) => self.control.set(true),
Some(ModifierKey::Command) => self.command.set(true),
None => {}
},
EventType::KeyRelease(key) => match modifier_key_from_key(*key) {
Some(ModifierKey::Shift) => self.shift.set(false),
Some(ModifierKey::Alt) => self.alt.set(false),
Some(ModifierKey::Control) => self.control.set(false),
Some(ModifierKey::Command) => self.command.set(false),
Some(ModifierKey::CapsLock) | None => {}
},
_ => {}
}
}
src/macos/simulate.rs:292
VirtualInput::simulatenow mutates the sharedCGEventSource's keyboard type viaCGEventSourceSetKeyboardTypeon every key event (throughset_keyboard_source_typeinconvert_native_with_source). This is a side effect on a value visible to other code paths (and observable to other callers that may share or query that source). It is also called from the globalsimulate()path even though the value is justcurrent_keyboard_type(), making it a no-op write. Consider only setting it when actually needed (e.g. when the event-source's current keyboard type differs fromkeyboard_type), and confining the mutation to keyboard events emitted viaVirtualInput.
match event_type {
EventType::KeyPress(_) | EventType::KeyRelease(_) => {
// Keep the source default and the event field aligned: event creation
// can read the source, while the field makes the final event explicit.
set_keyboard_source_type(source, keyboard_type);
}
_ => {}
}
src/macos/simulate.rs:446
update_modifier_stateis called aftercg_event.post(...). If posting succeeds but the event represented (for example) a Shift KeyPress, the modifier state used for the current call is correct (Shift wasn't yet recorded, and we don't translate modifier keys), but if the user's first event after construction is a non-modifier keypress that follows a modifier press in the samesimulatecall sequence, the ordering is fine. The subtle issue: ifconvert_native_with_sourcereturnsNone,update_modifier_stateis skipped — which means a failedKeyRelease(Shift)will leaveself.shiftstuck attrue, desynchronizing internal state from the OS. Consider updating modifier state regardless of whether event creation succeeded, or document this caveat.
pub fn simulate(&self, event_type: &EventType) -> Result<(), SimulateError> {
unsafe {
let keyboard_type = keyboard_type_value(self.keyboard_type);
if let Some(cg_event) = convert_native_with_source(
event_type,
&self.source,
keyboard_type,
Some(self.modifier_state()),
Some(&self.dead_state),
) {
cg_event.post(self.tap_loc);
self.update_modifier_state(event_type);
Ok(())
} else {
Err(SimulateError)
src/macos/simulate.rs:228
VirtualInput::simulatenow sets a UTF-16 string on every key-press event throughset_string_from_utf16_unchecked(when not suppressed by Cmd/Ctrl). This is a behavioral change for existing consumers: previously their synthesized key events carried only the keycode, and target apps interpreted them via their own key-translation. Now each event also carries an explicit Unicode payload, which many macOS text-input contexts prefer over the keycode. This may produce different text in apps that previously remapped keys (e.g., Vim with customlangmap, Karabiner-style remappers, IMEs). Consider exposing this via an opt-in setting (similar towith_keyboard_type) so callers who don't need it preserve the old behavior.
if options.keydown {
if let (Some(modifier_state), Some(dead_state)) = (modifier_state, dead_state) {
if should_translate_key_unicode(key, modifier_state) {
let mut dead_state_value = dead_state.get();
let unicode = unsafe {
unicode_from_code_with_keyboard_type(
keycode as u32,
modifier_state,
options.keyboard_type,
&mut dead_state_value,
)
};
dead_state.set(dead_state_value);
if let Some(unicode) = unicode {
if !unicode.unicode.is_empty() {
event.set_string_from_utf16_unchecked(&unicode.unicode);
}
}
}
}
src/macos/simulate.rs:188
- When a Cmd or Ctrl modifier is held, the unicode translation is skipped but the dead-key state is not updated either. If a user presses a dead key (e.g.,
´), then a Cmd-shortcut, thene, the dead-key state established by the dead-key press will persist across the suppressed Cmd event (correct) but if the dead-key itself happens while Cmd is held,dead_stateis not advanced asUCKeyTranslatewould naturally drop dead-key composition. This is a subtle interaction worth verifying; consider explicitly clearingdead_statewhen modifier_suppresses_unicode returns true, to avoid stale dead-key composition leaking into the next non-shortcut keystroke.
fn modifier_suppresses_unicode(modifier_state: u32) -> bool {
modifier_state & (MODIFIER_STATE_COMMAND | MODIFIER_STATE_CONTROL) != 0
}
fn should_translate_key_unicode(key: crate::Key, modifier_state: u32) -> bool {
modifier_key_from_key(key).is_none() && !modifier_suppresses_unicode(modifier_state)
}
src/macos/keyboard.rs:230
- Behavioral change in
Keyboard::modifier_state: previously whenalt && shift && !caps_lockthe value was10(Shift+Option), but with shift+caps_lock+alt it returned10regardless of caps_lock. The new implementation also includesMODIFIER_STATE_CAPS_LOCK(4) when caps_lock is on, producing2|4|8 = 14instead of the previous10. This changes the bit pattern passed toUCKeyTranslatefor caps_lock+option combinations and may change the translated character on layouts where Caps Lock affects Option-modified output. Please verify this change is intended (it appears to be a side effect of the refactor rather than a stated PR goal) and add a regression test for caps_lock+option translation.
fn modifier_state(&self) -> ModifierState {
let mut modifier_state = MODIFIER_STATE_NONE;
if self.shift {
modifier_state |= MODIFIER_STATE_SHIFT;
}
if self.caps_lock {
modifier_state |= MODIFIER_STATE_CAPS_LOCK;
}
if self.alt {
modifier_state |= MODIFIER_STATE_ALT;
}
modifier_state
}
src/macos/simulate.rs:180
modifier_key_from_keymapsKey::AltandKey::AltGrboth toModifierKey::Alt, but onlykVK_Option/kVK_RightOptionare handled in theRawKeyarms. There is no handling for theKey::Functionmodifier or forkVK_Functionraw keycodes, even though the code already lists Function inEventFieldtranslations elsewhere (seekVK_Functioninkeycodes/macos_virtual_keycodes.rs). For completeness and consistency with macOS shortcut conventions (Fn often suppresses character output), consider adding it.
fn modifier_key_from_key(key: crate::Key) -> Option<ModifierKey> {
match key {
crate::Key::ShiftLeft | crate::Key::ShiftRight => Some(ModifierKey::Shift),
crate::Key::Alt | crate::Key::AltGr => Some(ModifierKey::Alt),
crate::Key::CapsLock => Some(ModifierKey::CapsLock),
crate::Key::ControlLeft | crate::Key::ControlRight => Some(ModifierKey::Control),
crate::Key::MetaLeft | crate::Key::MetaRight => Some(ModifierKey::Command),
crate::Key::RawKey(RawKey::MacVirtualKeycode(keycode))
if keycode == kVK_Shift || keycode == kVK_RightShift =>
{
Some(ModifierKey::Shift)
}
crate::Key::RawKey(RawKey::MacVirtualKeycode(keycode))
if keycode == kVK_Option || keycode == kVK_RightOption =>
{
Some(ModifierKey::Alt)
}
crate::Key::RawKey(RawKey::MacVirtualKeycode(keycode)) if keycode == kVK_CapsLock => {
Some(ModifierKey::CapsLock)
}
crate::Key::RawKey(RawKey::MacVirtualKeycode(keycode))
if keycode == kVK_Control || keycode == kVK_RightControl =>
{
Some(ModifierKey::Control)
}
crate::Key::RawKey(RawKey::MacVirtualKeycode(keycode))
if keycode == kVK_Command || keycode == kVK_RightCommand =>
{
Some(ModifierKey::Command)
}
_ => None,
}
}
src/macos/keyboard.rs:192
- The
namefield of the returnedUnicodeInfois built fromString::from_utf16(&unicode), but ifunicodecontains C0 control characters they are not filtered out here (the early-return only handles the single-character case). The previous code had identical behavior, but the newpub(crate)extraction is now reachable fromsimulate.rswhere multi-character C0-only outputs would still be set as a string viaset_string_from_utf16_unchecked. Consider filtering control characters consistently for any length.
if length == 1 {
match String::from_utf16(&buff[..length]) {
Ok(s) => {
if let Some(c) = s.chars().next() {
if ('\u{1}'..='\u{1f}').contains(&c) {
return None;
}
}
}
Err(_) => {}
}
}
let unicode = buff[..length].to_vec();
Some(UnicodeInfo {
name: String::from_utf16(&unicode).ok(),
unicode,
is_dead: false,
})
7fc5898 to
67b29d1
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 9 comments.
Comments suppressed due to low confidence (2)
src/macos/simulate.rs:288
- The comment on lines 285–286 says "Keep the source default and the event field aligned", but the call on line 287 actually mutates the underlying
CGEventSourceto the requestedkeyboard_typeviaCGEventSourceSetKeyboardTypefor every keyboard event. The behavior is intentional, but the comment is misleading — please reword it to reflect that the source's keyboard type is being explicitly set to match the event field, rather than "kept default".
// Keep the source default and the event field aligned: event creation
// can read the source, while the field makes the final event explicit.
set_keyboard_source_type(source, keyboard_type);
}
src/macos/simulate.rs:446
update_modifier_stateis called only aftercg_event.post(self.tap_loc)succeeds. Ifconvert_native_with_sourcereturnsNone(e.g. for an unmapped key) the function returnsErr(SimulateError)and the modifier state is not updated. Worse, if posting a Shift KeyPress succeeds but a subsequent operation fails, no Shift KeyRelease bookkeeping ever happens for events the caller skipped. More concretely, the press/release of any non-modifier key whosekeycode_from_keyreturnsNonesimply returnsErrand any later modifier release won't be balanced by anything observable. Consider updating modifier state for every successfully posted event independently of intermediate failures, and double-checking the desired semantics when posting fails on a modifier key.
pub fn simulate(&self, event_type: &EventType) -> Result<(), SimulateError> {
unsafe {
let keyboard_type = keyboard_type_value(self.keyboard_type);
if let Some(cg_event) = convert_native_with_source(
event_type,
&self.source,
keyboard_type,
Some(self.modifier_state()),
Some(&self.dead_state),
) {
cg_event.post(self.tap_loc);
self.update_modifier_state(event_type);
Ok(())
} else {
Err(SimulateError)
}
}
67b29d1 to
58eba96
Compare
58eba96 to
627e986
Compare
627e986 to
360978c
Compare
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (6)
WalkthroughAdds a public ChangesmacOS Keyboard Type Feature
Sequence Diagram(s)sequenceDiagram
participant VirtualInput
participant convert_native_with_source
participant unicode_from_code_with_keyboard_type
participant CGEvent
participant FocusedApp
VirtualInput->>convert_native_with_source: keyboard_type, modifier_state, dead_state, EventType
convert_native_with_source->>unicode_from_code_with_keyboard_type: code, modifier_state, keyboard_type, &mut dead_state
unicode_from_code_with_keyboard_type-->>convert_native_with_source: UnicodeInfo / dead-state
convert_native_with_source->>CGEvent: set fields (keyboard_type, EVENT_SOURCE_USER_DATA), attach chars
CGEvent->>FocusedApp: post event
VirtualInput->>VirtualInput: update modifier masks and dead_state
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/macos/simulate.rs`:
- Around line 490-502: VirtualInput::simulate() posts CGEvents created by
convert_native_with_source without tagging them with EVENT_SOURCE_USER_DATA, so
callers that rely on set_keyboard_extra_info()/set_mouse_extra_info() cannot
detect self-injected events; fix by setting the event's user-data field to
EVENT_SOURCE_USER_DATA on the cg_event returned from convert_native_with_source
(the same way the top-level simulate() does) before calling
cg_event.post(self.tap_loc), keeping the following
self.update_modifier_state(event_type) call intact.
- Around line 125-127: current_keyboard_type currently passes
std::ptr::null_mut() to CGEventSourceGetKeyboardType; replace that with a real
CGEventSourceRef created via CGEventSourceCreate (e.g.,
CGEventSourceCreate(kCGEventSourceStatePrivate)), pass that ref into
CGEventSourceGetKeyboardType inside current_keyboard_type, and then release the
CF ref (CFRelease) before returning; update function to create, use, and clean
up the CGEventSourceRef rather than using null_mut().
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 028a9822-adc5-44ee-a75a-02eaacdee281
📒 Files selected for processing (6)
Cargo.tomlexamples/simulate_keyboard_type.rssrc/codes_conv.rssrc/lib.rssrc/macos/keyboard.rssrc/macos/simulate.rs
425f0c3 to
d932168
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 8 comments.
Comments suppressed due to low confidence (1)
src/macos/simulate.rs:510
- Modifier state is updated only after
convert_native_with_sourcesucceeds and the event is posted. If event creation fails (returns None),update_modifier_stateis skipped, so a failedsimulate(KeyPress(ShiftLeft))followed bysimulate(KeyPress(KeyA))will translate KeyA without Shift, while a corresponding KeyRelease(ShiftLeft) later would still be processed and could un-set state that was never set. Consider updating the tracked modifier state regardless of event creation success (or document/handle this drift explicitly), since the underlying physical key the caller intends to model is still being requested.
pub fn simulate(&self, event_type: &EventType) -> Result<(), SimulateError> {
unsafe {
let keyboard_type = keyboard_type_value(self.keyboard_type);
if let Some(cg_event) = convert_native_with_source(
event_type,
&self.source,
keyboard_type,
Some(self.modifier_state()),
Some(&self.dead_state),
) {
cg_event.post(self.tap_loc);
self.update_modifier_state(event_type);
Ok(())
} else {
Err(SimulateError)
}
d932168 to
d1c0c0e
Compare
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (1)
src/macos/simulate.rs (1)
125-127:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUse a real
CGEventSourceRefforCGEventSourceGetKeyboardType.
current_keyboard_type()still passesnull_mut()here. That leavesMacKeyboardType::Currentdependent on undocumented CoreGraphics behavior and can break both the top-levelsimulate()path andVirtualInputkeyboard-type resolution.Does Apple document passing NULL to `CGEventSourceGetKeyboardType`, or should callers create a `CGEventSourceRef` and pass that instead?🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/macos/simulate.rs` around lines 125 - 127, current_keyboard_type() must not pass null_mut() to CGEventSourceGetKeyboardType; instead create a real CGEventSourceRef (e.g., with CGEventSourceCreate(kCGEventSourceStateCombinedSessionState)), call CGEventSourceGetKeyboardType(source) with that ref, handle the case where creation fails (return a safe default or propagate an error), and release the source (CFRelease) after use; update the current_keyboard_type() implementation and any callers relying on MacKeyboardType::Current to use this new behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@examples/simulate_keyboard_type.rs`:
- Around line 55-60: Replace the unwrap() on VirtualInput::new so the example
handles creation failures gracefully: call rdev::VirtualInput::new(...) and
match or use ? to propagate the Result, then log or print an informative error
message and exit (or return Err) if creation fails; update the code that follows
(the variable virtual_input and its use with with_keyboard_type) to work with
the handled Result (e.g., bind on Ok(vi) then call
vi.with_keyboard_type(keyboard_type)). Ensure you reference VirtualInput::new
and with_keyboard_type when making the change.
In `@src/macos/keyboard.rs`:
- Line 262: Update the stale inline comment that says "ignore all modifiers for
name" to reflect actual behavior: the closure passes the computed modifier_state
(from flags_to_state(flags_bits)) into unicode_from_code, so modifiers are
respected; modify or remove the comment near the unicode_from_code call and the
variable modifier_state to accurately describe that the real modifier_state is
used rather than claiming modifiers are ignored.
In `@src/macos/simulate.rs`:
- Around line 503-509: The code repeatedly calls
keyboard_type_value(self.keyboard_type) for every event (in simulate.rs near
convert_native_with_source), causing MacKeyboardType::Current to be re-read per
event; change VirtualInput so that if its keyboard_type is
MacKeyboardType::Current you resolve and cache the actual system keyboard type
once (e.g., during VirtualInput construction or when keyboard_type is set) and
then use that cached resolved value in keyboard_type_value calls (replace
per-event keyboard_type_value(self.keyboard_type) with the stored resolved
keyboard type) while still passing modifier_state() and dead_state to
convert_native_with_source; update any fields (e.g., add a
cached_resolved_keyboard_type) and code paths that construct VirtualInput to
populate it.
- Around line 293-303: The function event_source_user_data currently reads the
globals MOUSE_EXTRA_INFO and KEYBOARD_EXTRA_INFO via unsafe static mut, causing
potential data races; change those statics to AtomicI64 (or AtomicIsize if
preferred) and update all accesses: replace unsafe reads in
event_source_user_data with atomic loads (e.g. .load(Ordering::SeqCst)) and
modify the public setter functions to use atomic stores (e.g. .store(value,
Ordering::SeqCst)); ensure you update the types/signatures where those statics
are referenced so no unsafe static mut reads/writes remain.
---
Duplicate comments:
In `@src/macos/simulate.rs`:
- Around line 125-127: current_keyboard_type() must not pass null_mut() to
CGEventSourceGetKeyboardType; instead create a real CGEventSourceRef (e.g., with
CGEventSourceCreate(kCGEventSourceStateCombinedSessionState)), call
CGEventSourceGetKeyboardType(source) with that ref, handle the case where
creation fails (return a safe default or propagate an error), and release the
source (CFRelease) after use; update the current_keyboard_type() implementation
and any callers relying on MacKeyboardType::Current to use this new behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 0604f0d4-4634-41ca-b94b-ac5a9199aea1
📒 Files selected for processing (6)
Cargo.tomlexamples/simulate_keyboard_type.rssrc/codes_conv.rssrc/lib.rssrc/macos/keyboard.rssrc/macos/simulate.rs
776c313 to
a33eec0
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 8 comments.
Comments suppressed due to low confidence (1)
src/macos/simulate.rs:525
update_modifier_stateis invoked only afterconvert_native_with_sourcereturnsSome(_)andcg_event.post()succeeds. If event creation fails for a modifier key (e.g.,keycode_from_keyreturnsNonefor somecrate::Keymapped to a modifier inmodifier_key_from_keybut not incode_from_key), the modifier state silently desynchronizes from the events the caller actually issued. Consider updating modifier state regardless of conversion success (or document that callers must guarantee every modifier key is convertible), so subsequent translations don't translate with a stale modifier mask.
pub fn simulate(&self, event_type: &EventType) -> Result<(), SimulateError> {
unsafe {
let keyboard_type = keyboard_type_value(self.keyboard_type);
if let Some(cg_event) = convert_native_with_source(
event_type,
&self.source,
keyboard_type,
Some(self.modifier_state()),
Some(&self.dead_state),
) {
cg_event.post(self.tap_loc);
self.update_modifier_state(event_type);
Ok(())
} else {
Err(SimulateError)
}
| match event_type { | ||
| EventType::KeyPress(_) | EventType::KeyRelease(_) => { | ||
| // Set both the source and event field to the requested keyboard type: | ||
| // event creation can read the source, while the field is explicit. | ||
| unsafe { | ||
| set_keyboard_source_type(source, keyboard_type); | ||
| } | ||
| }, | ||
| EventType::ButtonPress(button) => { | ||
| let point = get_current_mouse_location()?; | ||
| let event = match button { | ||
| Button::Left => CGEventType::LeftMouseDown, | ||
| Button::Right => CGEventType::RightMouseDown, | ||
| _ => return None, | ||
| }; | ||
| CGEvent::new_mouse_event( | ||
| source, | ||
| event, | ||
| point, | ||
| CGMouseButton::Left, // ignored because we don't use OtherMouse EventType | ||
| ) | ||
| .ok() | ||
| } | ||
| EventType::ButtonRelease(button) => { | ||
| let point = get_current_mouse_location()?; | ||
| let event = match button { | ||
| Button::Left => CGEventType::LeftMouseUp, | ||
| Button::Right => CGEventType::RightMouseUp, | ||
| _ => return None, | ||
| }; | ||
| CGEvent::new_mouse_event( | ||
| source, | ||
| event, | ||
| point, | ||
| CGMouseButton::Left, // ignored because we don't use OtherMouse EventType | ||
| ) | ||
| .ok() | ||
| } | ||
| EventType::MouseMove { x, y } => { | ||
| let point = CGPoint { x: (*x), y: (*y) }; | ||
| CGEvent::new_mouse_event(source, CGEventType::MouseMoved, point, CGMouseButton::Left) | ||
| .ok() | ||
| } | ||
| EventType::Wheel { delta_x, delta_y } => { | ||
| let wheel_count = 2; | ||
| CGEvent::new_scroll_event( | ||
| source, | ||
| ScrollEventUnit::PIXEL, | ||
| wheel_count, | ||
| (*delta_y).try_into().ok()?, | ||
| (*delta_x).try_into().ok()?, | ||
| 0, | ||
| ) | ||
| .ok() | ||
| } | ||
| _ => {} | ||
| } |
| /// State is updated only after an event is created and posted successfully. | ||
| /// `KeyRelease(CapsLock)` does not change the tracked Caps Lock state. | ||
| pub fn simulate(&self, event_type: &EventType) -> Result<(), SimulateError> { | ||
| unsafe { | ||
| if let Some(cg_event) = convert_native_with_source(event_type, self.source.clone()) { | ||
| let keyboard_type = keyboard_type_value(self.keyboard_type); | ||
| if let Some(cg_event) = convert_native_with_source( | ||
| event_type, | ||
| &self.source, | ||
| keyboard_type, | ||
| Some(self.modifier_state()), | ||
| Some(&self.dead_state), | ||
| ) { | ||
| cg_event.post(self.tap_loc); | ||
| self.update_modifier_state(event_type); |
| fn modifier_state(&self) -> ModifierState { | ||
| let mut modifier_state = MODIFIER_STATE_NONE; | ||
| if self.shift { | ||
| modifier_state |= MODIFIER_STATE_SHIFT; | ||
| } | ||
| if self.caps_lock { | ||
| modifier_state |= MODIFIER_STATE_CAPS_LOCK; | ||
| } | ||
| if self.alt { | ||
| modifier_state |= MODIFIER_STATE_ALT; | ||
| } | ||
| modifier_state | ||
| } |
| pub fn set_mouse_extra_info(extra: i64) { | ||
| unsafe { MOUSE_EXTRA_INFO = extra } | ||
| MOUSE_EXTRA_INFO.store(extra, Ordering::Relaxed); | ||
| } | ||
|
|
||
| /// Sets the macOS user-data tag used for simulated keyboard events. | ||
| /// | ||
| /// This is separate from `set_mouse_extra_info()`. | ||
| pub fn set_keyboard_extra_info(extra: i64) { | ||
| unsafe { KEYBOARD_EXTRA_INFO = extra } | ||
| KEYBOARD_EXTRA_INFO.store(extra, Ordering::Relaxed); | ||
| } |
| fn keycode_from_key(key: crate::Key) -> Option<CGKeyCode> { | ||
| match key { | ||
| crate::Key::RawKey(RawKey::MacVirtualKeycode(keycode)) => Some(keycode as _), | ||
| crate::Key::RawKey(_) => { | ||
| // Only macOS virtual keycodes can be converted into CG key events. | ||
| None | ||
| } | ||
| _ => code_from_key(key).map(|keycode| keycode as _), | ||
| } | ||
| } |
| fn current_keyboard_type() -> CGEventSourceKeyboardType { | ||
| unsafe { CGEventSourceGetKeyboardType(std::ptr::null_mut()) } | ||
| } |
| /// For key events, the underlying event source is updated when events are | ||
| /// sent through this `VirtualInput`. | ||
| /// The selected type is fixed for this `VirtualInput`; create another | ||
| /// instance to use a different keyboard type for a separate input stream. | ||
| /// Do not share the same `CGEventSource` with code that expects a different | ||
| /// keyboard type. | ||
| pub fn with_keyboard_type(mut self, keyboard_type: MacKeyboardType) -> Self { | ||
| self.keyboard_type = keyboard_type; |
a33eec0 to
1dfa9d6
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@examples/simulate_keyboard_type.rs`:
- Around line 28-37: The current send_with_input swallows SimulateError and
continues; change it to return a Result and propagate errors up through
send_key_sequence and send_keyboard_type_samples so the sample run stops on the
first failed input.simulate() call: modify send_with_input to return Result<(),
SimulateError>, replace the match that prints and continues with an early return
Err(error), and update callers (send_key_sequence, send_keyboard_type_samples)
to propagate the Result (using ? or explicit handling) and print one contextual
error message before returning to halt the run.
In `@src/macos/simulate.rs`:
- Around line 433-444: Update the doc comment for with_keyboard_type to state
that while setting keyboard_type on this VirtualInput fixes the type for
variants Ansi/ Iso/ Jis/ Raw(_), the MacKeyboardType::Current variant is
resolved dynamically per event (keyboard_type_value(self.keyboard_type) is
called on every simulate()), so Current behaves as a per-simulate/per-event
dynamic resolution rather than a fixed value; mention the simulate and
keyboard_type_value functions to make the exception explicit.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 45d521f9-7489-4ab7-bd54-f22845a39c30
📒 Files selected for processing (6)
Cargo.tomlexamples/simulate_keyboard_type.rssrc/codes_conv.rssrc/lib.rssrc/macos/keyboard.rssrc/macos/simulate.rs
Signed-off-by: fufesou <linlong1266@gmail.com>
1dfa9d6 to
154c762
Compare
Summary
This PR adds macOS support for simulating input with a specific hardware keyboard type, such as ANSI, ISO, or JIS.
MacKeyboardTypewithCurrent,Ansi,Iso,Jis, andRaw(u32)VirtualInput::with_keyboard_type()for selecting the keyboard type per virtual input source; the selected type is fixed for thatVirtualInputCGEventSourceand keyboard event fieldUCKeyTranslateVirtualInput::simulate()keyboard inputVirtualInput::simulate()--sendbefore sending real key eventsset_keyboard_extra_info()Compatibility notes
set_keyboard_extra_info()forEVENT_SOURCE_USER_DATA; callers that only setset_mouse_extra_info()must also setset_keyboard_extra_info()if they use extra info to identify self-injected keyboard events.alphaLockinstead of Shift in macOS Unicode translation. This is semantically correct, but it can change translated characters for Caps-Lock combinations in bothVirtualInput::simulate()and the existingKeyboard/KeyboardStatepath used bylistenconsumers.Notes
This does not switch the active macOS input source, keyboard layout, or IME. It only controls the hardware keyboard type used when translating physical keycodes.
VirtualInput::simulate()maintains modifier and dead-key state for a logical input stream. It should be called from the main thread for keyboard events because Unicode translation reads the active macOS input source through Carbon TIS APIs. Printable key presses set the event Unicode string fromUCKeyTranslateoutput, so they are not keycode-only events.Caps Lock state is internal to a
VirtualInputstream and toggles only onKeyPress(CapsLock);KeyRelease(CapsLock)does not change the tracked state. Callers that need matching Unicode output should route Caps Lock through the sameVirtualInput; external Caps Lock changes are not synchronized. Pending dead-key state is reset only when Unicode translation is suppressed for non-modifier Command/Control shortcuts.The selected keyboard type mutates the underlying
CGEventSourceused by theVirtualInputwhen key events are sent, and remains fixed for thatVirtualInput. Do not share that source with code that expects a different keyboard type.The top-level
simulate()function still uses the existing stateless conversion path. It now tags key events with the current macOS keyboard type on theCGEventSourceand keyboard event field. Stateful modifier/dead-key tracking and Command/Control Unicode suppression are provided byVirtualInput::simulate().Summary by CodeRabbit
New Features
Improvements
Documentation
Chores
Tests