Skip to content

Commit 9724ed4

Browse files
fix(deps): update rust crate cocoa to 0.26 (#63)
* fix(deps): update rust crate cocoa to 0.26 * fix: migrate from deprecated cocoa APIs to objc2 0.3 Replace deprecated cocoa 0.26 APIs with objc2-* equivalents across hotkey, main, and tray modules. ## Changes - Add objc2 0.5, objc2-app-kit 0.3, objc2-foundation 0.3 dependencies - hotkey.rs: Replace NSApp/NSAutoreleasePool/NSDate with objc2 equivalents - main.rs: Update NSApplication initialization and event loop to objc2 - tray.rs: Migrate NSScreen display scale detection to objc2 ## Implementation - Use MainThreadMarker for thread-safe API calls - Replace autoreleasepool block syntax (new/drain → autoreleasepool closure) - Update NSApplication activation policy to use enum - Replace NSArray FFI with iterator-based access ## Testing - cargo clippy passes without deprecation warnings - All unsafe blocks properly documented with safety comments Addresses cocoa 0.26 deprecation warnings requiring objc2-* crates. Signed-off-by: Marcin Skalski <[email protected]> --------- Signed-off-by: Marcin Skalski <[email protected]> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Marcin Skalski <[email protected]>
1 parent 9e03dbc commit 9724ed4

File tree

5 files changed

+140
-57
lines changed

5 files changed

+140
-57
lines changed

.claude/settings.local.json

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(grep:*)",
5+
"Bash(cargo test:*)",
6+
"Bash(find:*)",
7+
"Bash(cargo llvm-cov:*)",
8+
"Bash(ls:*)",
9+
"Bash(mkdir:*)",
10+
"Bash(gh issue view:*)",
11+
"Bash(git worktree:*)",
12+
"Bash(git checkout:*)",
13+
"Bash(git add:*)",
14+
"Bash(git commit -s -S -m \"$(cat <<''EOF''\nfeat: add Phase 1 test coverage improvements (47.80% → 59.90%)\n\nAdd 30+ unit tests across 5 modules to improve code coverage from 47.80% to 59.90% (+12.1pp).\n\n## Changes\n\n- Add mockall/mockito dev dependencies to Cargo.toml\n- permissions.rs: +6 tests (microphone, non-macOS platform checks)\n- telemetry.rs: +5 tests (path expansion edge cases)\n- transcription/download.rs: +6 tests (HTTP errors, filename generation)\n- input/cgevent.rs: +7 tests (UTF-16 encoding, text preview truncation)\n- audio/capture.rs: +8 tests (NaN/infinity handling, resampling precision)\n\n## Test Results\n\n- 92 passed, 34 ignored (integration tests)\n- Line coverage: 59.90% (was 47.80%)\n- Region coverage: 63.92%\n- Function coverage: 59.29%\n\n## Supporting documentation\n\nPart of Phase 1 implementation from test coverage improvement plan (see plans/test-coverage-improvement.md).\nEOF\n)\")",
15+
"Bash(git push:*)",
16+
"Bash(gh pr create:*)",
17+
"Bash(gh api:*)",
18+
"Bash(git commit -s -S -m \"$(cat <<''EOF''\nfix: address PR #28 review comments - remove low-value tests\n\nRemove redundant/duplicate tests and unused dependencies identified in PR review.\n\n## Changes\n\n- Remove unused mockall/mockito dev-dependencies (Phase 1 doesn''t need mocks)\n- Add #[ignore] to network-dependent test (consistent with similar tests)\n- Remove test_download_model_temp_file_extension (tests stdlib, not our code)\n- Remove 3 preview text tests (duplicated production logic)\n- Remove test_text_insertion_error_types (tests thiserror macro)\n- Fix test_save_wav_debug test to use temp_dir with timestamp (was fragile /nonexistent_directory/)\n- Remove test_request_all_permissions_calls_all_checks (redundant, no assertions)\n\n## Test Results\n\n- 185 passed, 80 ignored\n- All tests pass\n- -92 lines of low-value test code\n\n## Supporting documentation\n\nAddresses all 7 unresolved review comments from PR #28.\nEOF\n)\")",
19+
"Bash(cargo clippy:*)",
20+
"Bash(git commit:*)",
21+
"Bash(git reset:*)",
22+
"Bash(git cherry-pick:*)",
23+
"Bash(git pull:*)",
24+
"Bash(cat:*)",
25+
"Bash(git fetch:*)",
26+
"Bash(git merge-base:*)",
27+
"Bash(git rebase:*)",
28+
"Bash(cargo fmt:*)",
29+
"Bash(.git/hooks/pre-commit)",
30+
"Bash(gh pr view:*)",
31+
"Bash(cargo doc:*)",
32+
"Bash(git restore:*)",
33+
"Bash(cargo clean:*)",
34+
"Bash(RUST_LOG=debug cargo test:*)",
35+
"Bash(sips -g all:*)",
36+
"Bash(sips:*)",
37+
"Bash(cargo build:*)",
38+
"Bash(gh pr review:*)",
39+
"Bash(gh pr comment:*)",
40+
"Bash(./scripts/create-app-bundle.sh:*)",
41+
"Bash(codesign:*)",
42+
"Bash(plutil:*)",
43+
"Bash(tccutil reset:*)",
44+
"Bash(sqlite3:*)",
45+
"Bash(xattr:*)",
46+
"Bash(gh release list:*)",
47+
"Bash(gh release view:*)",
48+
"Bash(curl:*)",
49+
"Bash(brew tap:*)",
50+
"Bash(brew audit:*)",
51+
"Bash(brew info:*)",
52+
"Bash(brew install:*)",
53+
"Bash(brew uninstall:*)",
54+
"Bash(chmod:*)",
55+
"Bash(RUST_LOG=whisper_hotkey=debug cargo run:*)",
56+
"Bash(cargo tree:*)",
57+
"Bash(gh search:*)",
58+
"Bash(gh issue list:*)",
59+
"Bash(xxd:*)",
60+
"Bash(cargo check:*)",
61+
"Bash(cargo run:*)",
62+
"Bash(gh issue create:*)",
63+
"Bash(open target/llvm-cov/html/index.html)",
64+
"Bash(open:*)",
65+
"Bash(git revert:*)",
66+
"Bash(gh pr:*)"
67+
],
68+
"deny": [],
69+
"ask": []
70+
}
71+
}

Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ anyhow = "1"
1414
thiserror = "2"
1515
core-graphics = "0.25"
1616
core-foundation = "0.10"
17-
cocoa = "0.25"
17+
cocoa = "0.26"
18+
objc2 = "0.5"
19+
objc2-app-kit = "0.3"
20+
objc2-foundation = "0.3"
1821

1922
# Phase 2: Global Hotkey
2023
global-hotkey = "0.7"

src/input/hotkey.rs

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -384,31 +384,33 @@ impl MultiHotkeyManager {
384384
#[cfg(target_os = "macos")]
385385
#[allow(unsafe_code)] // Required for NSApplication event loop FFI
386386
fn pump_event_loop() {
387-
use cocoa::appkit::{NSApp, NSApplication};
388-
use cocoa::base::nil;
389-
use cocoa::foundation::{NSAutoreleasePool, NSDate};
387+
use objc2::rc::autoreleasepool;
388+
use objc2_app_kit::{NSApp, NSEventMask};
389+
use objc2_foundation::{MainThreadMarker, NSDate, NSDefaultRunLoopMode};
390390

391-
unsafe {
392-
let pool = NSAutoreleasePool::new(nil);
393-
let app = NSApp();
394-
let distant_past = NSDate::distantPast(nil);
391+
autoreleasepool(|_| {
392+
// Safety: This function is only called during initialization on the main thread
393+
let mtm = unsafe { MainThreadMarker::new_unchecked() };
394+
let app = NSApp(mtm);
395+
let distant_past = NSDate::distantPast();
395396

396397
// Process all pending events
397398
loop {
398-
let event = app.nextEventMatchingMask_untilDate_inMode_dequeue_(
399-
u64::MAX,
400-
distant_past,
401-
cocoa::foundation::NSDefaultRunLoopMode,
402-
true,
403-
);
404-
if event == nil {
399+
let event = unsafe {
400+
app.nextEventMatchingMask_untilDate_inMode_dequeue(
401+
NSEventMask(u64::MAX),
402+
Some(&distant_past),
403+
NSDefaultRunLoopMode,
404+
true,
405+
)
406+
};
407+
if let Some(event) = event {
408+
app.sendEvent(&event);
409+
} else {
405410
break;
406411
}
407-
app.sendEvent_(event);
408412
}
409-
410-
pool.drain();
411-
}
413+
});
412414
}
413415

414416
#[cfg(not(target_os = "macos"))]

src/main.rs

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,19 @@ use global_hotkey::GlobalHotKeyEvent;
3232
use std::sync::{Arc, Mutex};
3333

3434
#[cfg(target_os = "macos")]
35-
use cocoa::appkit::{NSApp, NSApplication, NSApplicationActivationPolicyAccessory};
35+
use objc2_app_kit::{NSApp, NSApplicationActivationPolicy};
3636
#[cfg(target_os = "macos")]
37-
use cocoa::base::nil;
37+
use objc2_foundation::MainThreadMarker;
3838

3939
#[tokio::main]
4040
async fn main() -> Result<()> {
4141
// macOS: Initialize NSApplication event loop (required for global-hotkey)
4242
#[cfg(target_os = "macos")]
43-
unsafe {
44-
let app = NSApp();
45-
app.setActivationPolicy_(NSApplicationActivationPolicyAccessory);
43+
{
44+
// Safety: main() runs on the main thread
45+
let mtm = unsafe { MainThreadMarker::new_unchecked() };
46+
let app = NSApp(mtm);
47+
app.setActivationPolicy(NSApplicationActivationPolicy::Accessory);
4648
}
4749
// Phase 1: Foundation
4850
// Load configuration
@@ -219,27 +221,33 @@ async fn main() -> Result<()> {
219221
loop {
220222
// macOS: Pump the event loop to process global hotkey events
221223
#[cfg(target_os = "macos")]
222-
unsafe {
223-
use cocoa::foundation::{NSAutoreleasePool, NSDate};
224+
{
225+
use objc2::rc::autoreleasepool;
226+
use objc2_app_kit::NSEventMask;
227+
use objc2_foundation::{NSDate, NSDefaultRunLoopMode};
224228

225-
let pool = NSAutoreleasePool::new(nil);
226-
let app = NSApp();
227-
let distant_past = NSDate::distantPast(nil);
229+
autoreleasepool(|_| {
230+
// Safety: Event loop runs on main thread
231+
let mtm = unsafe { MainThreadMarker::new_unchecked() };
232+
let app = NSApp(mtm);
233+
let distant_past = NSDate::distantPast();
228234

229-
loop {
230-
let event = app.nextEventMatchingMask_untilDate_inMode_dequeue_(
231-
u64::MAX,
232-
distant_past,
233-
cocoa::foundation::NSDefaultRunLoopMode,
234-
true,
235-
);
236-
if event == nil {
237-
break;
235+
loop {
236+
let event = unsafe {
237+
app.nextEventMatchingMask_untilDate_inMode_dequeue(
238+
NSEventMask(u64::MAX),
239+
Some(&distant_past),
240+
NSDefaultRunLoopMode,
241+
true,
242+
)
243+
};
244+
if let Some(event) = event {
245+
app.sendEvent(&event);
246+
} else {
247+
break;
248+
}
238249
}
239-
app.sendEvent_(event);
240-
}
241-
242-
pool.drain();
250+
});
243251
}
244252

245253
// Poll for hotkey events

src/tray.rs

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
use anyhow::{anyhow, Context, Result};
2-
use cocoa::appkit::NSScreen;
3-
use cocoa::base::id;
4-
use cocoa::foundation::NSArray;
2+
use objc2_app_kit::NSScreen;
53
use std::collections::HashMap;
64
use std::sync::{Arc, Mutex};
75
use tray_icon::menu::{Menu, MenuItem, PredefinedMenuItem};
@@ -55,19 +53,20 @@ impl TrayManager {
5553
/// # Safety
5654
/// Uses Cocoa FFI to query `NSScreen` backing scale factor:
5755
/// - `NSScreen::screens()` returns a retained `NSArray` (non-null by Cocoa contract)
58-
/// - `objectAtIndex(0)` returns a valid `NSScreen*` for the lifetime of this function
59-
/// - `backingScaleFactor` is a safe getter with no side effects
56+
/// - Array indexing is safe when `len()` > 0
57+
/// - `backingScaleFactor()` is a safe getter with no side effects
6058
fn detect_display_scale() -> f64 {
61-
unsafe {
62-
let screens = NSScreen::screens(cocoa::base::nil);
63-
// NSScreen::screens() returns an autoreleased NSArray (non-null by Cocoa)
64-
if screens.is_null() || NSArray::count(screens) == 0 {
65-
// Fallback to retina if no screens detected (most modern Macs)
66-
return 2.0;
67-
}
68-
let screen: id = screens.objectAtIndex(0);
69-
NSScreen::backingScaleFactor(screen)
70-
}
59+
use objc2_foundation::MainThreadMarker;
60+
61+
// Safety: This is called during initialization on the main thread
62+
let mtm = unsafe { MainThreadMarker::new_unchecked() };
63+
let screens = NSScreen::screens(mtm);
64+
65+
// Fallback to retina if no screens detected (most modern Macs)
66+
screens
67+
.iter()
68+
.next()
69+
.map_or(2.0, |screen| screen.backingScaleFactor())
7170
}
7271

7372
fn build_tray(

0 commit comments

Comments
 (0)