Skip to content

Commit 102de4a

Browse files
authored
Merge pull request #1 from msk1039/macos-fix
fixed the app for macos
2 parents 679dc6e + efbbe90 commit 102de4a

6 files changed

Lines changed: 129 additions & 7 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ x11rb = { version = "0.13", features = ["allow-unsafe-code"] }
4949
[target.'cfg(target_os = "macos")'.dependencies]
5050
objc2 = "0.5"
5151
objc2-app-kit = { version = "0.2", features = ["NSWorkspace", "NSRunningApplication"] }
52+
core-foundation = "0.9"
5253

5354
[profile.release]
5455
opt-level = 3

src/app.rs

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,21 @@ impl App {
3535
let app_filter = AppFilter::new(self.config.exclusions.apps.clone());
3636
let app_filter = Arc::new(Mutex::new(app_filter));
3737

38+
// On macOS, check for Accessibility permissions early and warn the user
39+
#[cfg(target_os = "macos")]
40+
{
41+
if !macos_accessibility_check() {
42+
log::error!(
43+
"Accessibility permissions NOT granted! Keycen requires Accessibility access \
44+
to intercept and simulate keystrokes. Please grant access in: \
45+
System Settings > Privacy & Security > Accessibility. \
46+
Add this application's binary to the list and restart."
47+
);
48+
} else {
49+
log::info!("Accessibility permissions verified ✓");
50+
}
51+
}
52+
3853
// Determine input mode
3954
let requested_mode = match self.config.general.mode.as_str() {
4055
"grab" => InputMode::Grab,
@@ -145,7 +160,7 @@ impl App {
145160
}
146161
});
147162

148-
// Main thread: pump Windows messages + handle tray menu events
163+
// Main thread: pump native messages + handle tray menu events
149164
let mut current_enabled = enabled;
150165
loop {
151166
// Pump Windows messages so the tray context menu works
@@ -163,6 +178,17 @@ impl App {
163178
}
164179
}
165180

181+
// On macOS, pump the main run loop so that:
182+
// 1. The system tray icon and context menu events are processed correctly
183+
// 2. Any main-thread dispatch queues can execute (needed by some frameworks)
184+
#[cfg(target_os = "macos")]
185+
{
186+
unsafe {
187+
use core_foundation::runloop::{kCFRunLoopDefaultMode, CFRunLoopRunInMode};
188+
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.05, 0);
189+
}
190+
}
191+
166192
if let Ok(event) = MenuEvent::receiver().try_recv() {
167193
if event.id == tray_menu.toggle_item.id() {
168194
current_enabled = !current_enabled;
@@ -202,6 +228,8 @@ impl App {
202228
}
203229
}
204230
// Small sleep to avoid busy-waiting
231+
// On macOS, CFRunLoopRunInMode already provides the delay
232+
#[cfg(not(target_os = "macos"))]
205233
thread::sleep(Duration::from_millis(50));
206234
}
207235
}
@@ -244,3 +272,79 @@ fn config_watch_loop(
244272
thread::sleep(Duration::from_secs(1));
245273
}
246274
}
275+
276+
/// Check if the process has Accessibility permissions on macOS.
277+
/// This is required for both listening to and simulating keyboard events.
278+
#[cfg(target_os = "macos")]
279+
fn macos_accessibility_check() -> bool {
280+
use std::ffi::c_void;
281+
282+
#[link(name = "ApplicationServices", kind = "framework")]
283+
extern "C" {
284+
fn AXIsProcessTrusted() -> bool;
285+
}
286+
287+
// Also try to prompt the user with the system dialog
288+
#[link(name = "CoreFoundation", kind = "framework")]
289+
extern "C" {
290+
fn CFStringCreateWithCString(
291+
alloc: *const c_void,
292+
c_str: *const u8,
293+
encoding: u32,
294+
) -> *const c_void;
295+
fn CFDictionaryCreate(
296+
allocator: *const c_void,
297+
keys: *const *const c_void,
298+
values: *const *const c_void,
299+
num_values: i64,
300+
key_callbacks: *const c_void,
301+
value_callbacks: *const c_void,
302+
) -> *const c_void;
303+
fn CFRelease(cf: *const c_void);
304+
static kCFTypeDictionaryKeyCallBacks: c_void;
305+
static kCFTypeDictionaryValueCallBacks: c_void;
306+
static kCFBooleanTrue: *const c_void;
307+
}
308+
309+
#[link(name = "ApplicationServices", kind = "framework")]
310+
extern "C" {
311+
fn AXIsProcessTrustedWithOptions(options: *const c_void) -> bool;
312+
}
313+
314+
unsafe {
315+
// First do a simple check
316+
let trusted = AXIsProcessTrusted();
317+
if trusted {
318+
return true;
319+
}
320+
321+
// If not trusted, try prompting the system dialog
322+
let key_cstr = b"AXTrustedCheckOptionPrompt\0";
323+
let key = CFStringCreateWithCString(
324+
std::ptr::null(),
325+
key_cstr.as_ptr(),
326+
0x08000100, // kCFStringEncodingUTF8
327+
);
328+
if !key.is_null() {
329+
let keys = [key];
330+
let values = [kCFBooleanTrue];
331+
let options = CFDictionaryCreate(
332+
std::ptr::null(),
333+
keys.as_ptr(),
334+
values.as_ptr(),
335+
1,
336+
&kCFTypeDictionaryKeyCallBacks as *const _,
337+
&kCFTypeDictionaryValueCallBacks as *const _,
338+
);
339+
if !options.is_null() {
340+
let result = AXIsProcessTrustedWithOptions(options);
341+
CFRelease(options);
342+
CFRelease(key);
343+
return result;
344+
}
345+
CFRelease(key);
346+
}
347+
348+
false
349+
}
350+
}

src/buffer/classifier.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@ pub enum KeyClass {
2020
}
2121

2222
/// Classify a key event into a buffer action
23-
pub fn classify_key(key: Key, name: Option<&str>, ctrl_held: bool) -> KeyClass {
24-
// Check for paste operation (Ctrl+V)
25-
if ctrl_held {
23+
pub fn classify_key(key: Key, name: Option<&str>, ctrl_held: bool, meta_held: bool) -> KeyClass {
24+
// Check for paste/undo/cut operations
25+
// On macOS, Cmd (Meta) is used instead of Ctrl for these shortcuts
26+
let shortcut_held = ctrl_held || meta_held;
27+
if shortcut_held {
2628
match key {
2729
Key::KeyV => return KeyClass::Paste,
2830
Key::KeyZ | Key::KeyX => return KeyClass::BufferReset,
29-
_ => return KeyClass::Ignore, // Other ctrl combos don't produce text
31+
_ => return KeyClass::Ignore, // Other shortcut combos don't produce text
3032
}
3133
}
3234

src/buffer/mod.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,16 @@ pub enum BufferAction {
2121
pub struct WordBuffer {
2222
buffer: String,
2323
ctrl_held: bool,
24+
/// On macOS, Cmd (Meta) is used instead of Ctrl for shortcuts like paste
25+
meta_held: bool,
2426
}
2527

2628
impl WordBuffer {
2729
pub fn new() -> Self {
2830
WordBuffer {
2931
buffer: String::with_capacity(64),
3032
ctrl_held: false,
33+
meta_held: false,
3134
}
3235
}
3336

@@ -39,10 +42,14 @@ impl WordBuffer {
3942
self.ctrl_held = true;
4043
return BufferAction::Ignored;
4144
}
45+
Key::MetaLeft | Key::MetaRight => {
46+
self.meta_held = true;
47+
return BufferAction::Ignored;
48+
}
4249
_ => {}
4350
}
4451

45-
let class = classify_key(key, name, self.ctrl_held);
52+
let class = classify_key(key, name, self.ctrl_held, self.meta_held);
4653

4754
match class {
4855
KeyClass::WordChar(ch) => {
@@ -77,6 +84,9 @@ impl WordBuffer {
7784
Key::ControlLeft | Key::ControlRight => {
7885
self.ctrl_held = false;
7986
}
87+
Key::MetaLeft | Key::MetaRight => {
88+
self.meta_held = false;
89+
}
8090
_ => {}
8191
}
8292
}

src/correction/mod.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ pub static IS_SIMULATING: AtomicBool = AtomicBool::new(false);
1616
pub static CORRECTION_IN_PROGRESS: AtomicBool = AtomicBool::new(false);
1717

1818
/// Delay between simulated keystrokes (ms)
19-
/// Keep this minimal to avoid interleaving with user typing
19+
/// macOS requires longer delays for the OS to process simulated events.
20+
/// The rdev docs recommend at least 20ms on macOS.
21+
#[cfg(target_os = "macos")]
22+
const SIMULATE_DELAY_MS: u64 = 20;
23+
#[cfg(not(target_os = "macos"))]
2024
const SIMULATE_DELAY_MS: u64 = 1;
2125

2226
/// Maximum word length we'll attempt to correct (safety limit)

0 commit comments

Comments
 (0)