Claude Code Plugins

Community-maintained marketplace

Feedback

macOS Cocoa and Objective-C interop for Rust applications

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name macos-cocoa-objc
description macOS Cocoa and Objective-C interop for Rust applications
triggers cocoa, objc, NSWindow, NSApplication, NSView, NSScreen, NSPasteboard, NSWorkspace, AppKit, vibrancy, window level, floating panel

macOS Cocoa/Objective-C Interop

This skill covers using the cocoa and objc crates to interact with macOS AppKit/Cocoa APIs from Rust. Script-kit-gpui uses these extensively for window management, system integration, and native UI features.

Crate Overview

cocoa crate (v0.26.x)

  • Purpose: Rust bindings to Cocoa/AppKit frameworks
  • Deprecated: In favor of objc2 crates, but still widely used
  • Modules:
    • cocoa::appkit - NSApp, NSWindow, NSScreen, NSPasteboard, etc.
    • cocoa::base - id, nil, YES, NO, BOOL
    • cocoa::foundation - NSString, NSRect, NSPoint, NSSize, NSArray
    • cocoa::quartzcore - Core Animation (CALayer, etc.)

objc crate (v0.2.x)

  • Purpose: Low-level Objective-C runtime bindings
  • Key macros: msg_send!, sel!, class!
  • Modules:
    • objc::runtime - Class, Object, Sel, Method, objc_getClass
    • objc::declare - ClassDecl for creating Objective-C classes
    • objc::rc - autoreleasepool

Key Concepts

Objective-C Messaging

All Objective-C method calls use message sending. In Rust:

use objc::{msg_send, sel, sel_impl, class};
use cocoa::base::{id, nil};

unsafe {
    // [NSApp sharedApplication]
    let app: id = msg_send![class!(NSApplication), sharedApplication];
    
    // [window setLevel:3]
    let _: () = msg_send![window, setLevel: 3i64];
    
    // [window frame] - returns NSRect
    let frame: NSRect = msg_send![window, frame];
    
    // [window isKeyWindow] - returns bool
    let is_key: bool = msg_send![window, isKeyWindow];
}

Selectors

Selectors are method name identifiers:

use objc::{sel, sel_impl};

let sel_frame = sel!(frame);
let sel_set_level = sel!(setLevel:);
let sel_set_frame_display = sel!(setFrame:display:);  // Multiple args

The id Type

id is a pointer to any Objective-C object (*mut objc::runtime::Object).

  • nil is the null pointer equivalent
  • Always check for null before using

Common AppKit Types

NSApplication (NSApp)

use cocoa::appkit::NSApp;
use cocoa::base::id;

unsafe {
    let app: id = NSApp();
    
    // Set activation policy (0=Regular, 1=Accessory, 2=Prohibited)
    let _: () = msg_send![app, setActivationPolicy: 1i64];
    
    // Check if active
    let is_active: bool = msg_send![app, isActive];
    
    // Get all windows
    let windows: id = msg_send![app, windows];
    let count: usize = msg_send![windows, count];
}

NSWindow

use cocoa::foundation::NSRect;

unsafe {
    // Window levels (NSWindowLevel)
    const NS_NORMAL_WINDOW_LEVEL: i64 = 0;
    const NS_FLOATING_WINDOW_LEVEL: i64 = 3;
    const NS_MODAL_PANEL_WINDOW_LEVEL: i64 = 8;
    const NS_POP_UP_MENU_WINDOW_LEVEL: i64 = 101;
    
    let _: () = msg_send![window, setLevel: NS_FLOATING_WINDOW_LEVEL];
    
    // Collection behaviors (bitflags)
    const MOVE_TO_ACTIVE_SPACE: u64 = 1 << 1;  // 2
    const FULL_SCREEN_AUXILIARY: u64 = 1 << 8; // 256
    const CAN_JOIN_ALL_SPACES: u64 = 1;
    const STATIONARY: u64 = 16;
    const IGNORES_CYCLE: u64 = 64;
    
    let current: u64 = msg_send![window, collectionBehavior];
    let new_behavior = current | MOVE_TO_ACTIVE_SPACE | FULL_SCREEN_AUXILIARY;
    let _: () = msg_send![window, setCollectionBehavior: new_behavior];
    
    // Frame operations
    let frame: NSRect = msg_send![window, frame];
    let _: () = msg_send![window, setFrame:new_frame display:true];
    
    // Visibility
    let _: () = msg_send![window, orderFront: nil];  // Show
    let _: () = msg_send![window, orderOut: nil];    // Hide
    let _: () = msg_send![window, makeKeyAndOrderFront: nil];
    
    // Properties
    let _: () = msg_send![window, setMovable: false];
    let _: () = msg_send![window, setOpaque: false];
    let _: () = msg_send![window, setHasShadow: true];
    let _: () = msg_send![window, setRestorable: false];
    let _: () = msg_send![window, setIgnoresMouseEvents: true];
}

NSScreen

use cocoa::appkit::NSScreen;

unsafe {
    // Get all screens
    let screens: id = NSScreen::screens(nil);
    let count: usize = msg_send![screens, count];
    
    // Get main screen (primary display)
    let main_screen: id = NSScreen::mainScreen(nil);
    let frame: NSRect = msg_send![main_screen, frame];
    let visible_frame: NSRect = msg_send![main_screen, visibleFrame];
}

NSPasteboard (Clipboard)

use cocoa::appkit::NSPasteboard;

unsafe {
    let pasteboard: id = NSPasteboard::generalPasteboard(nil);
    
    // Efficient change detection (no payload read)
    let change_count: i64 = msg_send![pasteboard, changeCount];
}

NSWorkspace

unsafe {
    let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace];
    
    // Get frontmost app
    let app: id = msg_send![workspace, frontmostApplication];
    let menu_owner: id = msg_send![workspace, menuBarOwningApplication];
    
    // App info
    let bundle_id: id = msg_send![app, bundleIdentifier];
    let name: id = msg_send![app, localizedName];
    let pid: i32 = msg_send![app, processIdentifier];
}

NSColor

unsafe {
    // System colors
    let clear: id = msg_send![class!(NSColor), clearColor];
    let window_bg: id = msg_send![class!(NSColor), windowBackgroundColor];
    
    // Custom RGBA
    let color: id = msg_send![class!(NSColor),
        colorWithRed: 0.5f64
        green: 0.5f64
        blue: 0.5f64
        alpha: 0.8f64
    ];
}

NSVisualEffectView (Vibrancy/Blur)

// Materials (NSVisualEffectMaterial)
const POPOVER: isize = 6;
const SIDEBAR: isize = 7;
const HUD_WINDOW: isize = 13;

// Blending modes
const BEHIND_WINDOW: isize = 0;
const WITHIN_WINDOW: isize = 1;

// States
const FOLLOWS_WINDOW: isize = 0;
const ACTIVE: isize = 1;
const INACTIVE: isize = 2;

unsafe {
    let _: () = msg_send![effect_view, setMaterial: POPOVER];
    let _: () = msg_send![effect_view, setBlendingMode: BEHIND_WINDOW];
    let _: () = msg_send![effect_view, setState: FOLLOWS_WINDOW];
    let _: () = msg_send![effect_view, setEmphasized: true];
}

NSAppearance

#[link(name = "AppKit", kind = "framework")]
extern "C" {
    static NSAppearanceNameDarkAqua: id;
    static NSAppearanceNameVibrantDark: id;
    static NSAppearanceNameAqua: id;
    static NSAppearanceNameVibrantLight: id;
}

unsafe {
    let appearance: id = msg_send![
        class!(NSAppearance),
        appearanceNamed: NSAppearanceNameVibrantDark
    ];
    let _: () = msg_send![window, setAppearance: appearance];
}

Usage in script-kit-gpui

Window Level and Floating Panels

// From src/platform.rs - configure as floating panel
pub fn configure_as_floating_panel() {
    unsafe {
        let window = get_main_window()?;
        
        // Float above normal windows
        let _: () = msg_send![window, setLevel: 3i64];
        
        // Move to active space when shown
        let current: u64 = msg_send![window, collectionBehavior];
        let desired = current | 2 | 256;  // MoveToActiveSpace | FullScreenAuxiliary
        let _: () = msg_send![window, setCollectionBehavior: desired];
        
        // Disable restoration
        let _: () = msg_send![window, setRestorable: false];
    }
}

HUD Windows (Click-Through Overlays)

// From src/hud_manager.rs
unsafe {
    let _: () = msg_send![window, setLevel: 101i64];  // PopUpMenuLevel
    
    // Behaviors for HUD
    let behaviors: u64 = 1 | 16 | 64;  // CanJoinAllSpaces | Stationary | IgnoresCycle
    let _: () = msg_send![window, setCollectionBehavior: behaviors];
    
    // Click-through for non-interactive HUDs
    let _: () = msg_send![window, setIgnoresMouseEvents: true];
    
    // Show without activating
    let _: () = msg_send![window, orderFront: nil];
}

Vibrancy Material Configuration

// From src/platform.rs - match Raycast/Spotlight appearance
pub fn configure_window_vibrancy_material() {
    unsafe {
        // Force VibrantDark appearance
        let appearance: id = msg_send![
            class!(NSAppearance),
            appearanceNamed: NSAppearanceNameVibrantDark
        ];
        let _: () = msg_send![window, setAppearance: appearance];
        
        // Window background for native border
        let bg: id = msg_send![class!(NSColor), windowBackgroundColor];
        let _: () = msg_send![window, setBackgroundColor: bg];
        let _: () = msg_send![window, setOpaque: false];
        let _: () = msg_send![window, setHasShadow: true];
    }
}

Accessory App (No Dock Icon)

// From src/platform.rs - LSUIElement equivalent at runtime
pub fn configure_as_accessory_app() {
    unsafe {
        let app: id = NSApp();
        // NSApplicationActivationPolicyAccessory = 1
        let _: () = msg_send![app, setActivationPolicy: 1i64];
    }
}

Unsafe Patterns

Basic Pattern

#[cfg(target_os = "macos")]
pub fn do_cocoa_thing() {
    unsafe {
        // All Cocoa calls are unsafe
    }
}

#[cfg(not(target_os = "macos"))]
pub fn do_cocoa_thing() {
    // No-op on other platforms
}

Null Checking

unsafe {
    let window: id = msg_send![app, keyWindow];
    if window.is_null() {
        return None;  // Handle gracefully
    }
    // Safe to use window
}

Type Annotations are Required

// WRONG - compiler can't infer return type
let result = msg_send![obj, someMethod];

// CORRECT - explicit type annotation
let result: id = msg_send![obj, someMethod];
let _: () = msg_send![obj, setFoo: bar];  // void returns need ()

Integer Types Matter

// NSInteger is i64 on 64-bit macOS
let _: () = msg_send![window, setLevel: 3i64];

// NSUInteger is u64
let behavior: u64 = msg_send![window, collectionBehavior];

// Some APIs use isize
let material: isize = msg_send![effect_view, material];

Memory Management

Autorelease Pools

Required when creating Objective-C objects on background threads:

use objc::rc::autoreleasepool;

std::thread::spawn(|| {
    autoreleasepool(|| unsafe {
        // Create NSStrings, etc. here
        let string: id = msg_send![class!(NSString), stringWithUTF8String: "hello"];
        // Objects are released when pool drains
    });
});

When Pools are Needed

  • Background threads without existing pool
  • Notification callbacks
  • Any code that creates many temporary Objective-C objects

Manual Retain/Release (Rare)

unsafe {
    let obj: id = msg_send![class!(SomeClass), alloc];
    let obj: id = msg_send![obj, init];
    // obj has +1 retain count
    
    let _: () = msg_send![obj, release];  // -1 retain count
}

Threading

Main Thread Requirement

CRITICAL: AppKit APIs (NSApp, NSWindow, NSScreen, etc.) are NOT thread-safe and MUST be called from the main thread.

#[cfg(target_os = "macos")]
fn debug_assert_main_thread() {
    unsafe {
        let is_main: bool = msg_send![class!(NSThread), isMainThread];
        debug_assert!(
            is_main,
            "AppKit calls must run on the main thread"
        );
    }
}

pub fn some_appkit_function() {
    debug_assert_main_thread();
    unsafe {
        // Safe to call AppKit APIs
    }
}

Thread-Safe Wrappers

For storing window IDs across threads:

#[derive(Debug, Clone, Copy)]
struct WindowId(usize);

impl WindowId {
    fn from_id(window: id) -> Self {
        Self(window as usize)
    }
    fn to_id(self) -> id {
        self.0 as id
    }
}

// Safe because we only store the ID, not access the window
unsafe impl Send for WindowId {}
unsafe impl Sync for WindowId {}

Background Observers

For notification observers on background threads:

std::thread::spawn(|| {
    setup_workspace_observer();  // Creates run loop
});

fn setup_workspace_observer() {
    autoreleasepool(|| unsafe {
        // Create observer class
        // Register for notifications
        // Run the run loop
    });
}

Creating Objective-C Classes

For notification observers or delegates:

use objc::declare::ClassDecl;
use objc::runtime::{Class, Object, Sel};

unsafe {
    let superclass = Class::get("NSObject").unwrap();
    
    // Check if class already exists
    let observer_class = if let Some(existing) = Class::get("MyObserver") {
        existing
    } else {
        let mut decl = ClassDecl::new("MyObserver", superclass).unwrap();
        
        // Add method
        extern "C" fn handle_notification(
            _this: &Object,
            _sel: Sel,
            notification: *mut Object,
        ) {
            let _ = std::panic::catch_unwind(|| {
                autoreleasepool(|| unsafe {
                    // Handle notification
                });
            });
        }
        
        decl.add_method(
            sel!(handleNotification:),
            handle_notification as extern "C" fn(&Object, Sel, *mut Object),
        );
        
        decl.register()
    };
}

NSString Conversion

Rust String to NSString

unsafe fn rust_to_nsstring(s: &str) -> id {
    let c_str = std::ffi::CString::new(s).unwrap();
    msg_send![class!(NSString), stringWithUTF8String: c_str.as_ptr()]
}

NSString to Rust String

unsafe fn nsstring_to_rust(nsstring: id) -> Option<String> {
    if nsstring.is_null() {
        return None;
    }
    let c_str: *const std::os::raw::c_char = msg_send![nsstring, UTF8String];
    if c_str.is_null() {
        return None;
    }
    Some(std::ffi::CStr::from_ptr(c_str).to_string_lossy().into_owned())
}

Anti-patterns

Don't Use keyWindow During Startup

// WRONG - keyWindow may be nil during startup
let window: id = msg_send![app, keyWindow];

// CORRECT - use a window registry
let window = window_manager::get_main_window()?;

Don't Forget Return Type Annotations

// WRONG - won't compile
msg_send![window, setLevel: 3];

// CORRECT
let _: () = msg_send![window, setLevel: 3i64];

Don't Call AppKit from Background Threads

// WRONG - will crash or produce undefined behavior
std::thread::spawn(|| {
    let app: id = NSApp();  // BAD!
});

// CORRECT - use main thread
cx.spawn(|mut cx| async move {
    cx.update(|cx| {
        // AppKit calls here are on main thread
    });
});

Don't Ignore Platform Checks

// WRONG - won't compile on other platforms
use cocoa::base::id;  // Only exists on macOS

// CORRECT
#[cfg(target_os = "macos")]
use cocoa::base::id;

#[cfg(target_os = "macos")]
pub fn macos_only_function() { ... }

#[cfg(not(target_os = "macos"))]
pub fn macos_only_function() {
    // No-op or appropriate fallback
}

Don't Swizzle Without Checking

// WRONG - may swizzle multiple times
pub fn swizzle_method() {
    // swizzle code
}

// CORRECT - use atomic flag
static SWIZZLE_DONE: AtomicBool = AtomicBool::new(false);

pub fn swizzle_method() {
    if SWIZZLE_DONE.swap(true, Ordering::SeqCst) {
        return;  // Already done
    }
    // swizzle code
}

Coordinate System

macOS uses a bottom-left origin coordinate system, opposite of most UI frameworks:

/// Convert from AppKit (bottom-left origin) to top-left origin
fn flip_y(screen_height: f64, y: f64, height: f64) -> f64 {
    screen_height - y - height
}

/// Get primary screen height for coordinate conversion
fn primary_screen_height() -> Option<f64> {
    unsafe {
        let screens: id = NSScreen::screens(nil);
        if screens.is_null() { return None; }
        let primary: id = msg_send![screens, objectAtIndex: 0usize];
        if primary.is_null() { return None; }
        let frame: NSRect = msg_send![primary, frame];
        Some(frame.size.height)
    }
}

Quick Reference

Task Code
Get app NSApp()
Get window msg_send![app, keyWindow]
Set level msg_send![window, setLevel: 3i64]
Show window msg_send![window, orderFront: nil]
Hide window msg_send![window, orderOut: nil]
Get frame msg_send![window, frame] -> NSRect
Is main thread msg_send![class!(NSThread), isMainThread] -> bool
Null check if obj.is_null() { return; }
Platform guard #[cfg(target_os = "macos")]