Skip to content

feat(macOS): support configurable keyboard types for simulated input#2

Open
fufesou wants to merge 1 commit into
masterfrom
feat/macOS-input-keyboard-type
Open

feat(macOS): support configurable keyboard types for simulated input#2
fufesou wants to merge 1 commit into
masterfrom
feat/macOS-input-keyboard-type

Conversation

@fufesou
Copy link
Copy Markdown
Owner

@fufesou fufesou commented May 14, 2026

Summary

This PR adds macOS support for simulating input with a specific hardware keyboard type, such as ANSI, ISO, or JIS.

  • Add MacKeyboardType with Current, Ansi, Iso, Jis, and Raw(u32)
  • Add VirtualInput::with_keyboard_type() for selecting the keyboard type per virtual input source; the selected type is fixed for that VirtualInput
  • Apply the selected keyboard type to both the CGEventSource and keyboard event field
  • Use the selected keyboard type when translating keycodes through UCKeyTranslate
  • Track modifier and dead-key state for VirtualInput::simulate() keyboard input
  • Preserve pending dead-key state across modifier-only key presses, including when Command/Control is held
  • Avoid injecting printable Unicode for Command/Control shortcuts in VirtualInput::simulate()
  • Add a macOS example for manually verifying ANSI/ISO/JIS keyboard behavior; it requires --send before sending real key events
  • Fix macOS keyboard extra info so keyboard events use set_keyboard_extra_info()

Compatibility notes

  • macOS keyboard extra info is now split by event kind. Keyboard events use set_keyboard_extra_info() for EVENT_SOURCE_USER_DATA; callers that only set set_mouse_extra_info() must also set set_keyboard_extra_info() if they use extra info to identify self-injected keyboard events.
  • Caps Lock now uses Carbon alphaLock instead of Shift in macOS Unicode translation. This is semantically correct, but it can change translated characters for Caps-Lock combinations in both VirtualInput::simulate() and the existing Keyboard/KeyboardState path used by listen consumers.

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 from UCKeyTranslate output, so they are not keycode-only events.

Caps Lock state is internal to a VirtualInput stream and toggles only on KeyPress(CapsLock); KeyRelease(CapsLock) does not change the tracked state. Callers that need matching Unicode output should route Caps Lock through the same VirtualInput; 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 CGEventSource used by the VirtualInput when key events are sent, and remains fixed for that VirtualInput. 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 the CGEventSource and keyboard event field. Stateful modifier/dead-key tracking and Command/Control Unicode suppression are provided by VirtualInput::simulate().

Summary by CodeRabbit

  • New Features

    • Per-stream macOS keyboard layout selection (Current, ANSI, ISO, JIS, Raw) for simulated input.
    • Example CLI to optionally send real key sequences to the focused app (opt-in via flag).
  • Improvements

    • More accurate modifier and dead-key handling, including suppression for Command/Control and correct dead-key preservation/clearing.
    • Virtual input instances now track keyboard type and modifier state for realistic event sequences.
  • Documentation

    • Added macOS keyboard simulation example with timing/focus guidance.
  • Chores

    • Added a macOS-only native dependency.
  • Tests

    • Expanded macOS-focused tests for keyboard behavior.

Review Change Stack

@fufesou fufesou requested a review from Copilot May 14, 2026 02:22
@fufesou
Copy link
Copy Markdown
Owner Author

fufesou commented May 14, 2026

@coderabbitai review

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 MacKeyboardType enum exported from the crate root and consumed by VirtualInput.
  • simulate.rs refactored: keyboard event creation extracted into helpers, modifier/dead-key state tracked via Cell fields, and EVENT_SOURCE_USER_DATA now selected per event kind.
  • keyboard.rs gains a shared unicode_from_code_with_keyboard_type helper and reworks Keyboard::modifier_state to 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_state is called after convert_native_with_source has 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, if cg_event.post() succeeds but produces no event (None branch), the modifier state is not updated, while if post() returns Err later 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 in modifier_state, but keyboard_event_from_key skips unicode generation for KeyRelease (only keydown does 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.

Comment thread src/macos/keyboard.rs
Comment thread src/macos/simulate.rs
Comment thread src/macos/simulate.rs Outdated
Comment thread src/macos/simulate.rs Outdated
Comment thread src/macos/simulate.rs
Comment thread src/macos/simulate.rs
Comment thread src/macos/simulate.rs
Comment thread src/macos/simulate.rs
Comment thread src/macos/keyboard.rs Outdated
Comment thread src/macos/simulate.rs
@fufesou fufesou force-pushed the feat/macOS-input-keyboard-type branch from 1c3283a to 9fff887 Compare May 14, 2026 02:52
@fufesou fufesou requested a review from Copilot May 14, 2026 02:52
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, but update_modifier_state is also skipped entirely when convert_native_with_source returns None (e.g. an unmapped key, or CGEvent::new_keyboard_event failure). 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)
            }

Comment thread src/macos/simulate.rs
Comment thread src/macos/simulate.rs
Comment thread src/macos/simulate.rs
Comment thread src/macos/simulate.rs
Comment thread src/macos/keyboard.rs
Comment thread src/macos/simulate.rs
Comment thread src/macos/keyboard.rs
@fufesou fufesou force-pushed the feat/macOS-input-keyboard-type branch from 9fff887 to 7fc5898 Compare May 14, 2026 03:17
@fufesou fufesou requested a review from Copilot May 14, 2026 03:40
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_state does not distinguish left/right variants of a modifier. If both ShiftLeft and ShiftRight are pressed and only one is released, self.shift is 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::simulate now mutates the shared CGEventSource's keyboard type via CGEventSourceSetKeyboardType on every key event (through set_keyboard_source_type in convert_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 global simulate() path even though the value is just current_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 from keyboard_type), and confining the mutation to keyboard events emitted via VirtualInput.
    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_state is called after cg_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 same simulate call sequence, the ordering is fine. The subtle issue: if convert_native_with_source returns None, update_modifier_state is skipped — which means a failed KeyRelease(Shift) will leave self.shift stuck at true, 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::simulate now sets a UTF-16 string on every key-press event through set_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 custom langmap, Karabiner-style remappers, IMEs). Consider exposing this via an opt-in setting (similar to with_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, then e, 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_state is not advanced as UCKeyTranslate would naturally drop dead-key composition. This is a subtle interaction worth verifying; consider explicitly clearing dead_state when 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 when alt && shift && !caps_lock the value was 10 (Shift+Option), but with shift+caps_lock+alt it returned 10 regardless of caps_lock. The new implementation also includes MODIFIER_STATE_CAPS_LOCK (4) when caps_lock is on, producing 2|4|8 = 14 instead of the previous 10. This changes the bit pattern passed to UCKeyTranslate for 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_key maps Key::Alt and Key::AltGr both to ModifierKey::Alt, but only kVK_Option/kVK_RightOption are handled in the RawKey arms. There is no handling for the Key::Function modifier or for kVK_Function raw keycodes, even though the code already lists Function in EventField translations elsewhere (see kVK_Function in keycodes/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 name field of the returned UnicodeInfo is built from String::from_utf16(&unicode), but if unicode contains 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 new pub(crate) extraction is now reachable from simulate.rs where multi-character C0-only outputs would still be set as a string via set_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,
    })

Comment thread src/macos/simulate.rs
Comment thread src/macos/keyboard.rs Outdated
@fufesou fufesou force-pushed the feat/macOS-input-keyboard-type branch from 7fc5898 to 67b29d1 Compare May 14, 2026 04:25
@fufesou fufesou requested a review from Copilot May 14, 2026 04:25
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 CGEventSource to the requested keyboard_type via CGEventSourceSetKeyboardType for 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_state is called only after cg_event.post(self.tap_loc) succeeds. If convert_native_with_source returns None (e.g. for an unmapped key) the function returns Err(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 whose keycode_from_key returns None simply returns Err and 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)
            }
        }

Comment thread src/macos/simulate.rs
Comment thread src/macos/simulate.rs
Comment thread src/macos/simulate.rs
Comment thread src/macos/simulate.rs
Comment thread src/macos/simulate.rs Outdated
Comment thread src/macos/simulate.rs
Comment thread src/macos/simulate.rs
Comment thread src/macos/keyboard.rs
Comment thread src/macos/simulate.rs
@fufesou fufesou force-pushed the feat/macOS-input-keyboard-type branch from 67b29d1 to 58eba96 Compare May 14, 2026 05:57
@fufesou fufesou requested a review from Copilot May 14, 2026 05:57
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 7 comments.

Comment thread src/macos/keyboard.rs
Comment thread src/macos/simulate.rs
Comment thread src/macos/simulate.rs
Comment thread src/macos/simulate.rs Outdated
Comment thread src/macos/simulate.rs
Comment thread src/macos/simulate.rs
Comment thread src/macos/simulate.rs Outdated
@fufesou fufesou force-pushed the feat/macOS-input-keyboard-type branch from 58eba96 to 627e986 Compare May 14, 2026 07:09
@fufesou fufesou requested a review from Copilot May 14, 2026 07:09
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 13 comments.

Comment thread src/macos/simulate.rs
Comment thread src/macos/simulate.rs Outdated
Comment thread src/macos/simulate.rs Outdated
Comment thread src/macos/simulate.rs
Comment thread src/macos/keyboard.rs
Comment thread src/macos/simulate.rs
Comment thread src/macos/simulate.rs Outdated
Comment thread src/macos/simulate.rs Outdated
Comment thread examples/simulate_keyboard_type.rs
Comment thread src/macos/simulate.rs
@fufesou fufesou force-pushed the feat/macOS-input-keyboard-type branch from 627e986 to 360978c Compare May 14, 2026 10:07
@fufesou fufesou requested a review from Copilot May 14, 2026 10:08
@fufesou
Copy link
Copy Markdown
Owner Author

fufesou commented May 14, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 14, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 14, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: de9e2082-ed48-4d2e-a321-af086b2e6b00

📥 Commits

Reviewing files that changed from the base of the PR and between 1dfa9d6 and 154c762.

📒 Files selected for processing (6)
  • Cargo.toml
  • examples/simulate_keyboard_type.rs
  • src/codes_conv.rs
  • src/lib.rs
  • src/macos/keyboard.rs
  • src/macos/simulate.rs

Walkthrough

Adds a public MacKeyboardType, refactors macOS keyboard translation to accept keyboard-type and manage dead-key/modifier state, makes VirtualInput a stateful per-stream translator with a with_keyboard_type() builder, expands tests, adds a macOS example, and updates a macOS-only dependency.

Changes

macOS Keyboard Type Feature

Layer / File(s) Summary
Public API and Dependencies
src/lib.rs, Cargo.toml
Introduces MacKeyboardType non-exhaustive public enum (Current, Ansi, Iso, Jis, Raw(u32)) and adds foreign-types = "0.3.2" to macOS target deps.
Keyboard translation helper and modifier constants
src/macos/keyboard.rs
Adds unicode_from_code_with_keyboard_type() helper, MODIFIER_STATE_* Carbon bit constants, updates UCKeyTranslate usage, changes dead-key/no-char handling, and optimizes UTF-16 decoding.
Conversion pipeline and VirtualInput state
src/macos/simulate.rs
Rewrites conversion to accept keyboard type and state, sets event keyboard-type and event-source fields, routes printable Unicode via the new helper, suppresses translation for Command/Control, preserves/clears dead-state appropriately, and turns VirtualInput into a stateful per-stream controller with with_keyboard_type().
Tests and test helpers
src/macos/simulate.rs (tests)
Expands tests with CoreGraphics FFI and ExtraInfoGuard covering keyboard-type mapping, Unicode suppression under modifiers, modifier tracking and release semantics, dead-key preservation/clearing, and event-source user-data correctness.
Small fixes
src/codes_conv.rs
Fixes test casting in test_usb_hid_code_to_macos_code to compare u32::from(code) with code2 as u32.
Example: simulate keyboard by layout
examples/simulate_keyboard_type.rs
New macOS-only example that sends sample key sequences for ANSI/ISO/JIS via VirtualInput, gated by --send, with inter-event delays and focus guidance. Non-macOS main prints availability.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

A rabbit hops across ANSI, ISO, JIS keys,
Tuning dead-keys, modifiers, and gentle decrees.
Streams remember layouts, caps, and state,
Virtual paws type with timing innate.
🐰 Click—events fly to the focused screen.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: adding support for configurable keyboard types on macOS for simulated input, which is the core objective of this PR.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/macOS-input-keyboard-type

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between a90dbe1 and 425f0c3.

📒 Files selected for processing (6)
  • Cargo.toml
  • examples/simulate_keyboard_type.rs
  • src/codes_conv.rs
  • src/lib.rs
  • src/macos/keyboard.rs
  • src/macos/simulate.rs

Comment thread src/macos/simulate.rs
Comment thread src/macos/simulate.rs
@fufesou fufesou force-pushed the feat/macOS-input-keyboard-type branch from 425f0c3 to d932168 Compare May 15, 2026 05:18
@fufesou fufesou requested a review from Copilot May 15, 2026 05:18
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_source succeeds and the event is posted. If event creation fails (returns None), update_modifier_state is skipped, so a failed simulate(KeyPress(ShiftLeft)) followed by simulate(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)
            }

Comment thread src/macos/simulate.rs
Comment thread src/macos/simulate.rs
Comment thread src/macos/keyboard.rs
Comment thread src/macos/simulate.rs
Comment thread src/macos/simulate.rs
Comment thread src/macos/simulate.rs
Comment thread src/macos/simulate.rs
Comment thread examples/simulate_keyboard_type.rs Outdated
@fufesou fufesou requested a review from Copilot May 15, 2026 05:42
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 16 comments.

Comment thread src/macos/simulate.rs
Comment thread src/macos/simulate.rs
Comment thread src/macos/simulate.rs
Comment thread src/macos/simulate.rs
Comment thread src/macos/simulate.rs
Comment thread src/macos/simulate.rs
Comment thread src/macos/simulate.rs
Comment thread examples/simulate_keyboard_type.rs Outdated
Comment thread src/macos/simulate.rs
Comment thread src/macos/simulate.rs Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (1)
src/macos/simulate.rs (1)

125-127: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use a real CGEventSourceRef for CGEventSourceGetKeyboardType.

current_keyboard_type() still passes null_mut() here. That leaves MacKeyboardType::Current dependent on undocumented CoreGraphics behavior and can break both the top-level simulate() path and VirtualInput keyboard-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

📥 Commits

Reviewing files that changed from the base of the PR and between 425f0c3 and d1c0c0e.

📒 Files selected for processing (6)
  • Cargo.toml
  • examples/simulate_keyboard_type.rs
  • src/codes_conv.rs
  • src/lib.rs
  • src/macos/keyboard.rs
  • src/macos/simulate.rs

Comment thread examples/simulate_keyboard_type.rs Outdated
Comment thread src/macos/keyboard.rs Outdated
Comment thread src/macos/simulate.rs
Comment thread src/macos/simulate.rs
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 7 comments.

Comment thread src/macos/simulate.rs
Comment thread src/macos/simulate.rs
Comment thread src/macos/simulate.rs
Comment thread examples/simulate_keyboard_type.rs Outdated
Comment thread src/macos/keyboard.rs
Comment thread src/macos/simulate.rs
Comment thread src/macos/simulate.rs
@fufesou fufesou force-pushed the feat/macOS-input-keyboard-type branch 2 times, most recently from 776c313 to a33eec0 Compare May 15, 2026 07:13
@fufesou fufesou requested a review from Copilot May 15, 2026 07:13
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_state is invoked only after convert_native_with_source returns Some(_) and cg_event.post() succeeds. If event creation fails for a modifier key (e.g., keycode_from_key returns None for some crate::Key mapped to a modifier in modifier_key_from_key but not in code_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)
            }

Comment thread src/macos/simulate.rs
Comment thread src/macos/simulate.rs
Comment thread src/macos/keyboard.rs
Comment thread src/macos/simulate.rs
Comment thread src/macos/simulate.rs
Comment thread src/macos/simulate.rs
Comment thread Cargo.toml
Comment thread examples/simulate_keyboard_type.rs Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 7 comments.

Comment thread src/macos/simulate.rs
Comment on lines +329 to 338
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()
}
_ => {}
}
Comment thread src/macos/simulate.rs Outdated
Comment on lines +508 to +521
/// 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);
Comment thread src/macos/keyboard.rs
Comment on lines +228 to +240
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
}
Comment thread src/macos/simulate.rs
Comment on lines 81 to 90
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);
}
Comment thread src/macos/simulate.rs
Comment on lines +152 to +161
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 _),
}
}
Comment thread src/macos/simulate.rs
Comment on lines +132 to +134
fn current_keyboard_type() -> CGEventSourceKeyboardType {
unsafe { CGEventSourceGetKeyboardType(std::ptr::null_mut()) }
}
Comment thread src/macos/simulate.rs
Comment on lines +436 to +443
/// 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;
@fufesou fufesou force-pushed the feat/macOS-input-keyboard-type branch from a33eec0 to 1dfa9d6 Compare May 15, 2026 14:01
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between d1c0c0e and 1dfa9d6.

📒 Files selected for processing (6)
  • Cargo.toml
  • examples/simulate_keyboard_type.rs
  • src/codes_conv.rs
  • src/lib.rs
  • src/macos/keyboard.rs
  • src/macos/simulate.rs

Comment thread examples/simulate_keyboard_type.rs Outdated
Comment thread src/macos/simulate.rs
Signed-off-by: fufesou <linlong1266@gmail.com>
@fufesou fufesou force-pushed the feat/macOS-input-keyboard-type branch from 1dfa9d6 to 154c762 Compare May 15, 2026 15:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants