macos-events
v0.0.1
Published
Native macOS event streams and automation primitives for Node.js
Downloads
19
Maintainers
Readme
macos-events
Native macOS desktop automation primitives for TypeScript and JavaScript.
Use macos-events to build window managers, desktop controllers, hotkey/input tools, status agents, and other local automation software in JS runtimes.
The project is currently macOS-only and early-stage. It exposes window and display discovery, Accessibility-based window control, permission checks, and native event streams for windows, global input, apps, displays, spaces, sleep/wake, and session lock state.
Requirements
- macOS 12+
- Node.js 18.17+
- Xcode Command Line Tools
- Accessibility permission for window control and Accessibility window events
- Input Monitoring permission for global mouse, keyboard, scroll, and tablet events
Install / Build
npm installThe install script builds src/native/addon.mm into build/Release/macos_events.node with node-gyp.
Useful scripts:
npm run build # build native addon and compiled JavaScript
npm run typecheck # type-check TypeScript sources
npm test # run the smoke test
npm run playground # inspect windows/screens and stream eventsUsage
import * as wm from "macos-events";
// if the process doesn't have accessibilityPermission, then request it
if (!wm.hasAccessibilityPermission()) wm.requestAccessibilityPermission();
// get the focused window, then set the size of it
const focused = wm.getFocusedWindow();
if (focused) {
wm.setWindowFrame(focused.id, { x: 0, y: 0, width: 960, height: 1080 });
}
// logs the name of the window when it's focused
wm.addEventListener("window-focused", (event) => {
console.log(event.window.appName, event.window.title);
});Permissions
macOS treats window automation and global input observation as separate privacy domains.
| Permission | Used for | Backing API |
| ---------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| Accessibility | getFocusedWindow(), focusWindow(), setWindowFrame(), and window events | Accessibility API via AXUIElement |
| Input Monitoring | Mouse, keyboard, scroll, and tablet events | Quartz Event Services listen-only event taps |
requestAccessibilityPermission() opens the system Accessibility prompt by calling AXIsProcessTrustedWithOptions. requestInputMonitoringPermission() opens the Input Monitoring prompt through CGRequestListenEventAccess. In both cases, macOS grants permission to the terminal or app hosting the process, not to this package directly.
API
hasAccessibilityPermission(): boolean
requestAccessibilityPermission(): boolean
hasInputMonitoringPermission(): boolean
requestInputMonitoringPermission(): boolean
listScreens(): ScreenInfo[]
listWindows(): WindowInfo[]
getFocusedWindow(): WindowInfo | null
focusWindow(id: number): boolean
setWindowFrame(id: number, frame: Rect): boolean
addEventListener<T extends EventType>(type: T, listener: EventListener<T>): () => void
removeEventListener<T extends EventType>(type: T, listener: EventListener<T>): voidWindows are identified by their CoreGraphics window ID. listWindows() returns visible, non-desktop windows. setWindowFrame() requires a positive width and height. Window control methods throw if the target window cannot be found or macOS rejects the Accessibility operation.
addEventListener() starts the relevant native event stream on first subscription and stops it when the last listener for that stream is removed. It returns an unsubscribe function.
Events
Window events require Accessibility permission.
| Event | Description |
| ---------------------- | ---------------------------------------- |
| window-created | A window was created by an observed app. |
| window-destroyed | A window was closed or destroyed. |
| window-moved | A window position changed. |
| window-resized | A window size changed. |
| window-minimized | A window was minimized. |
| window-restored | A minimized window was restored. |
| window-focused | The focused window changed. |
| window-title-changed | A window title changed. |
Input events require Input Monitoring permission and are delivered through a listen-only CGEventTapCreate event tap. The addon observes input; it does not intercept or mutate it.
| Event | Description |
| --------------------- | ------------------------------------------------------ |
| mouse-down | Any mouse button was pressed. |
| mouse-up | Any mouse button was released. |
| mouse-moved | The pointer moved without a button drag. |
| mouse-dragged | The pointer moved while a button was held. |
| left-mouse-down | The left mouse button was pressed. |
| left-mouse-up | The left mouse button was released. |
| left-mouse-dragged | The pointer moved while the left button was held. |
| right-mouse-down | The right mouse button was pressed. |
| right-mouse-up | The right mouse button was released. |
| right-mouse-dragged | The pointer moved while the right button was held. |
| other-mouse-down | A non-left/right mouse button was pressed. |
| other-mouse-up | A non-left/right mouse button was released. |
| other-mouse-dragged | The pointer moved while another mouse button was held. |
| scroll | A scroll wheel or trackpad scroll occurred. |
| key-down | A keyboard key was pressed. |
| key-up | A keyboard key was released. |
| flags-changed | Modifier key state changed. |
| tablet-point | Tablet pointer data changed. |
| tablet-proximity | Tablet proximity state changed. |
System events do not use either explicit prompt helper in this package.
| Event | Description |
| ----------------------- | -------------------------------------- |
| app-launched | An app started. |
| app-terminated | An app exited. |
| app-activated | An app became active. |
| app-deactivated | An app stopped being active. |
| app-hidden | An app was hidden. |
| app-unhidden | A hidden app was shown. |
| display-added | A display was connected. |
| display-removed | A display was disconnected. |
| display-moved | A display frame origin changed. |
| display-resized | A display mode or size changed. |
| display-enabled | A display was enabled. |
| display-disabled | A display was disabled. |
| display-reconfigured | A display changed in any reported way. |
| active-space-changed | The active macOS Space changed. |
| screens-did-sleep | Displays went to sleep. |
| screens-did-wake | Displays woke from sleep. |
| system-will-sleep | The system is about to sleep. |
| system-did-wake | The system woke from sleep. |
| system-will-power-off | The system is about to power off. |
| session-did-lock | The user session locked. |
| session-did-unlock | The user session unlocked. |
Event Shapes
wm.addEventListener("window-focused", (event) => {
console.log(event.window.id, event.window.appName, event.window.title);
});
wm.addEventListener("key-down", (event) => {
console.log(event.keyCode, event.flags, event.repeat);
});
wm.addEventListener("display-reconfigured", (event) => {
console.log(event.displayId, event.flags);
});Window events include a window snapshot. Input events share one shape across mouse, keyboard, scroll, and tablet events. System events include app, displayId, or flags when relevant.
Playground
npm run playgroundThe playground prints the current Accessibility trust state, screens, focused window, and visible windows, then streams native window, input, and system events until stopped with Ctrl+C.
macOS Notes
Window enumeration uses public CoreGraphics APIs. Window control uses Accessibility APIs and maps Accessibility windows to CoreGraphics IDs with _AXUIElementGetWindow, a private macOS symbol commonly used by window-management tools. This is practical for local automation and window-manager tooling, but it is not App Store-safe.
Window events use native Accessibility observers rather than polling. System events use NSWorkspace notifications, distributed lock/unlock notifications, and CoreGraphics display reconfiguration callbacks.
Roadmap
I'd be cool to, at some point, expand this package to Linux and Windows, but currently macOS is the priority
TODO
- [ ] Fix deprecated warnings
- [ ] publish to npm + edit the readme to include npm install commands
