Skip to content
macos-cocoa-objc logo

macOS Cocoa/Objective-C Interop

macos-cocoa-objc

macOS Cocoa and Objective-C interop for Rust applications

SKILL.md

Full skill instructions

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

TaskCode
Get appNSApp()
Get windowmsg_send![app, keyWindow]
Set levelmsg_send![window, setLevel: 3i64]
Show windowmsg_send![window, orderFront: nil]
Hide windowmsg_send![window, orderOut: nil]
Get framemsg_send![window, frame] -> NSRect
Is main threadmsg_send![class!(NSThread), isMainThread] -> bool
Null checkif obj.is_null() { return; }
Platform guard#[cfg(target_os = "macos")]

More skills from majiayu000

xiaohongshu logo
majiayu000/claude-arsenal

xiaohongshu

xiaohongshu

286 148
View
agent-task-conductor logo
majiayu000/claude-skill-registry

agent-task-conductor

Conduct multi-agent task orchestration and workflow coordination.

663 1
View
conductor-setup logo
majiayu000/claude-skill-registry

conductor-setup

Initialize project with Conductor artifacts (product definition,

663 1
View
animation-designer logo
majiayu000/claude-skill-registry

animation-designer

Expert in web animations, transitions, and motion design using Framer Motion and CSS

663 1
View
diagramming logo
majiayu000/claude-skill-registry

diagramming

Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...

663 1
View
h3-pg logo
majiayu000/claude-skill-registry-data

h3-pg

PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.

23 1
View
conductor-development logo
majiayu000/claude-skill-registry

conductor-development

Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...

663 1
View
conductor-status logo
majiayu000/claude-skill-registry

conductor-status

Display project status, active tracks, and next actions

663 1
View
dockerization logo
majiayu000/claude-skill-registry

dockerization

Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.

663 1
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View