Hello old-browser-using friend! Despite the mid-2000s aesthetics here, this site is actually built mainly with modern web technologies. So, if you're not using one the site will probably look broken.
I try to make it work as much possible where the fixes are simple, and the site shouldn't be so broken as to be unusable (let me know if it is) - but my sanity and experience for modern browsers trumps supporting IE7 or whatever.

2026-04-06-objc-js.md - Notepad

Apple, apparently, lets you write macOS ObjC code in JavaScript.

• filed under macos, notes, macos, objc, js

I like JavaScript (well, TypeScript, but close enough), and I have primarily been using macOS for about 10 years at this point, and as someone who has a penchant for writing the most cursed & evil code possible, I have had multiple attempts to interact with macOS' native APIs from JavaScript. Now, being on macOS makes this interesting, because of ObjC (which to clarify, I use to refer to the runtime & C API exposed by libobjc.dylib, not the Objective-C language it is intended to be used with), which allows almost all APIs on the system to be interfaced with with only a handful of C functions, primarily objc_msgSend, which is what Objective-C's... distinctive [] notation is actually syntactic sugar for. The only problem is that a bunch of FFI implementations in JS have bad support for things like varargs, and there wasn't really a good way to write a abstraction layer that let you use different ones without having to touch the code of the library, so after not being able to get much further than displaying a single empty window, those projects got shelved.

Another interesting thing about macOS, is AppleScript and the OSA, which for the uninitiated, the Open Scripting Architecture (OSA) is a standard that macOS applications can adopt to expose an interface so that other applications & scripts can retrieve information from & perform actions in those apps, with AppleScript being a "natural language" (or as much of that as you can expect for a language introduced in 1993) programming language that allows you to write scripts to interface with OSA supporting applications. It is a really nice idea which, like a lot of the nice things about macOS, I don't think any modern developer would implement if they were building the OS today, and as such almost any app that doesn't have a lineage that dates back at least 10 years is almost guaranteed to have absolutely no support.

Problem is, as someone who's used to modern, C-like programming languages, AppleScript is not exactly familliar. No worries though, in OS X 10.10 (2014), Apple introduced JavaScript for Automation (JXA, where the 'X' came from, I have no idea.), which allowed you to harness the OSA with a more modern, actively developed language, and unlike the Windows equivilent (Windows Script Host/WSH), isn't stuck in the 90's and uses the same JavaScriptCore framework thats used by the Safari web browser. But, due the lack of documentation & non-standard behaviour of the OSA objects, I generally stuck with AppleScript due to it being much more documented with code samples on forums for example, or upgrading full fledged C# with it's mostly-fully-featured bindings to the macOS SDK (which are still called Xamarin in some places despite Xamarin being discontinued, and the actual package being absurdly named Microsoft.macOS), which combined with ScriptingBridge which provides an ObjC interface for OSA.

The Nerd Snipe of the Century

That was, until I reread the docs today and saw this:

var fileManager = $.NSFileManager.defaultManager
[...]
if (fileManager.fileExistsAtPathIsDirectory(item.toString(), isDir) && isDir[0]) {

Mac Automation Scripting Guide: Processing Dropped Files and Folders

which I recognised that as looking awfully like a ObjC API. And after checking, found it matched the NSFileManager fileExistsAtPath:isDirectory: function.

This was the nerdsnipe of the century.

I quickly tried to find the documentation mentioning this $ global, and found it. In the JavaScript for Automation release notes for OS X 10.10, there it was, the documentation for an Objective-C bridge.

When you give someone like me this information, I will immediately go to drop everything I was working on, and I MUST figure this out and spend hours trying until I do figure it out. The thing I knew I needed to figure out was display a window, and then put a web view in there, because Electron is disgustingly sensible and I must figure out ways to take the least sensible option.

And after a while I figured it out. After much trial and error (mainly because trying to search for how to do anything with AppKit gives you results for SwiftUI, and when you exclude those gives you results for UIKit, which is the just-similar-enough-to-be-confusing iOS UI framework, and excluding those gives you a barren search results page with only things you've already tried).

ObjC.import('Cocoa')
ObjC.import('WebKit')

// Create a window
var win = $.NSWindow.alloc.initWithContentRectStyleMaskBackingDefer(
	$.NSMakeRect(0,0,600,500),
	$.NSWindowStyleMaskTitled | $.NSWindowStyleMaskClosable | $.NSWindowStyleMaskResizable,
	$.NSBackingStoreBuffered,
	false
)


// Create a webview
var config = $.WKWebViewConfiguration.alloc.init;
config.applicationNameForUserAgent = "Version/26.4 Safari/605.1.15"; // prevents sites (like google) from assuming we're using an old crusty browser. update as needed.
var webview = $.WKWebView.alloc.initWithFrameConfiguration(win.contentView.frame, config);
webview.loadHTMLStringBaseURL(
	`<meta http-equiv="refresh" content="0; url=https://google.com/">`, 
	$.NSURL.alloc.initWithString("about:blank")
);
// replace the entire window content with the webview, filling the window even when resized
win.contentView = webview


// Register notification handler for updating the window title
ObjC.registerSubclass({
    name: 'TitleUpdateReciever',
    superclass: 'NSObject',
    methods: {
	    'observeValueForKeyPath:ofObject:change:context:': function (keyPath, ofObject, change, context) {
            win.title = webview.title
        },
    }
})
webview.addObserverForKeyPathOptionsContext(
	$.TitleUpdateReciever.alloc.init,
	"title",
	$.NSKeyValueObservingOptionNew,
	null
)



// and finally focus the window
win.makeKeyAndOrderFront(win);

Important thing to note is that if you try and run a JS app that creates a window from the script editor, it will fail due to not being on the primary thread. You need to save the script as an application (with 'Stay open after run handler' checked) and then use Cmd+Opt+R to run the application, and you have to use Cmd+Q to quit the script application first or your code won't rerun when you try to Cmd+Opt+R.

Is this code amazing? No. Will some veteran OS X developer blow up my email inbox telling me how stupid I am? Maybe. (if you are said developer, maybe do get in touch, but don't be too harsh plskthx 🥺)

But it works. And also I have spent way longer on this than I wished and I was planning to do other things (remember how I started this by actually needing to write an AppleScript for something?)

Conclusion

Is this useful? Maybe. You still have to keep in mind, that just because it's JavaScript, that doesn't mean it's the Web, or Node. You don't get require() or ES Modules. You don't get the DOM, Fetch, fs.readFileSync(). You have to use the functions supplied by macOS' SDK (like NSURLRequest instead of Fetch) to do anything. Which means my initial idea of allowing you to interface with OSA from Node/Deno/Bun still isn't dead. Which I am oh so pleased to hear.