Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

74 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

MacroMac Automation Engine Banner

MacroMac

Live Showcase

Open-source JSON macro runner for macOS desktop automation.

MacroMac demo

MacroMac records mouse and keyboard input over live applications, stores it as editable JSON, and replays it with simple synchronization blocks: delays, shell commands, file signals, and polling conditions.

It is universal in the low-level sense: it does not need app-specific APIs, browser extensions, or per-site integrations. If a workflow can be driven by mouse, keyboard, files, and shell commands, Macro can usually represent it.

It is intentionally low-level. MacroMac does not understand UI semantics, recover from changed layouts, or know whether the correct window is focused.

Status

Experimental macOS tooling for personal automation, prototyping, and script-driven desktop workflows.

Why It Exists

Most automation tools choose one world:

  • app-specific APIs, accessibility trees, or browser automation, which can be robust but require a matching integration;
  • consumer macro recorders, which are convenient but opaque and hard to generate or edit as data;
  • scripts and local agents, which are programmable but cannot always press real GUI controls.

MacroMac is the narrow bridge:

real GUI input -> editable JSON -> shell/file synchronization -> headless replay

This is the macro-as-code paradigm: record the physical workflow once, then inspect, edit, generate, version, and run it like any other local program.

MacroMac's original value is this small contract: physical desktop input becomes editable data, and external programs can synchronize with it through simple file and shell handoff points.

That shape is useful for GUI-only workflows: games, legacy apps, chat clients, installers, local tools, and sites where API access is unavailable, unwanted, or slower than using the screen.

The goal is not smarter automation inside the core. The goal is honest low-level automation: record real input, expose it as plain data, and let external tools provide intelligence through files, shell commands, condition, or signal.

What It Does

  • Opens a native macOS launcher for choosing, editing, and running macro JSON files.
  • Records live mouse, click, and keyboard input into step blocks.
  • Replays real input through macOS event APIs.
  • Stores workflows as plain JSON in blocks.
  • Supports fixed delays, shell commands, file-change signals, and shell polling conditions.
  • Discards human recording time; playback timing is controlled by pace.
  • Runs in headless runner and hotkey supervisor modes.
  • Lets each macro define its own launch hotkey.

What It Is Not

  • Not image recognition.
  • Not OCR.
  • Not accessibility-tree automation.
  • Not browser automation like Selenium or Playwright.
  • Not a full RPA platform.
  • Not an API replacement when a stable API exists.
  • Not resilient to arbitrary app layout changes.
  • Not safe to run with untrusted JSON.

Requirements

  • macOS 13 or newer.
  • Swift 5.9+ / Xcode Command Line Tools.

Install command line tools if needed:

xcode-select --install

Download

For normal use, download the built MacroMac-macos-universal.zip asset from GitHub Releases, unzip it, and open Macro.command.

The GitHub-generated "Source code" archives are not ready-to-run app builds. Use the attached release zip.

The release build is a universal macOS binary for Apple Silicon and Intel Macs. It is unsigned and not notarized, so macOS may require manual approval on first launch.

Security And Privacy

Macro is powerful because it can observe and synthesize real desktop input.

macOS may ask for:

  • Accessibility - allows Macro to move the cursor, click, and type.
  • Input Monitoring - allows Macro to observe keyboard and hardware key events. Grant permissions in System Settings -> Privacy & Security -> Accessibility / Input Monitoring. Depending on how you launch Macro, macOS may ask you to grant permission to Terminal, the Swift-built macro binary, or both. Relaunch Macro after changing permissions.

Macro JSON is trusted code:

  • step blocks can click and type into real applications.
  • command and condition blocks run through $SHELL -lc, falling back to /bin/zsh -lc.
  • Macros can alter the clipboard, move files, delete files, submit forms, or trigger app actions.

Only run macro files you understand. Do not run untrusted JSON.

Recorded macros can also reveal private workflow details: screen coordinates, app layout, keyboard actions, file paths, and shell commands. This repository intentionally ignores macros/*.json.

Quick Start

From the repository root:

swift run macro

This opens the native launcher. From there:

  • New / Overwrite opens the editor for a new or selected macro.
  • Edit Steps rewrites only the existing step blocks in a selected macro.
  • Run starts the selected macro in runner mode.
  • Open JSON opens the selected macro file in your default editor.
  • Hotkeys starts the background supervisor for macros with top-level hotkey.

The launcher reads ./macros/*.json relative to the current working directory.

Finder Launchers

The repository includes simple double-click launchers:

  • Macro.command - opens the launcher.
  • Example.command - runs the safe example macro from macros/example.json.
  • Hotkeys.command - starts the background hotkey supervisor.

These are plain shell scripts. macOS may require confirmation the first time you open them.

Standalone Build

The development launchers use swift run, so the development machine needs the Swift toolchain.

To create a folder that can be copied to another Mac without installing Swift there:

./PackageRelease.command

This creates dist/MacroMac/ and dist/MacroMac-macos-universal.zip with:

  • macro - release executable.
  • Macro.command - opens the launcher using the bundled executable.
  • Example.command - runs the bundled example macro.
  • Hotkeys.command - starts the bundled hotkey supervisor.
  • macros/example.json - safe sample macro.
  • macro.config.example.json - optional controls template.

The target Mac still needs compatible macOS permissions for Accessibility and Input Monitoring. For public distribution outside your own machines, code signing and notarization are recommended to avoid Gatekeeper warnings.

Controls

In the overlay editor:

  • F1 - start or stop recording the current step.
  • F2 - cancel the current recorded step and record the same step again.
  • F3 - run or stop playback.
  • F4 - quit.

In runner mode:

  • F3 - run or stop the loaded macro.
  • F4 - quit.

Top-row function keys are fragile on macOS. Depending on keyboard settings and hardware, brightness/Mission Control/Launchpad keys may map to the same physical keys. Input Monitoring permission may be required for reliable handling.

Editor and runner controls can be changed in macro.config.json:

{
  "version": 1,
  "controls": {
    "record": { "key": "F1" },
    "cancelStep": { "key": "F2" },
    "playback": { "key": "F3" },
    "quit": { "key": "F4" }
  }
}

If macro.config.json is missing, these defaults are used. macro.config.example.json is committed as a template; macro.config.json is local and ignored by git.

Hotkeys use:

{ "key": "F8", "modifiers": ["control", "option"] }

or a raw macOS virtual key code:

{ "keyCode": 100, "modifiers": ["control", "option"] }

Supported named keys include F1 through F20, A through Z, 0 through 9, space, tab, return, escape, delete, left, right, up, and down. Supported modifiers are command, control, option, shift, and function.

Modes

Open launcher:

swift run macro

Open editor directly:

swift run macro -- --editor

Record into a specific file:

swift run macro -- --editor --macro my-flow.json

Rewrite only existing step blocks:

swift run macro -- --editor --rewrite-steps --macro my-flow.json

Run a macro in headless mode and wait for F3:

swift run macro -- --runner --macro my-flow.json

Run immediately:

swift run macro -- --runner --play --macro my-flow.json

Start the hotkey supervisor:

swift run macro -- --hotkeys

In this mode MacroMac scans ./macros/*.json, loads every macro with a top-level hotkey, and keeps running in the background. Pressing a macro hotkey starts that macro. Pressing the same hotkey again stops it. Pressing another macro hotkey stops the current macro and starts the new one.

Restart the supervisor after adding, removing, or changing macro hotkey fields.

Local shortcut mode:

swift run macro -- --dialog dialog.json

--dialog is a shortcut for headless runner plus immediate play. If no file is provided, it uses macros/dialog.json. This is intended for local custom scenarios; dialog.json is ignored by git.

Workflow

  1. Record one or more step blocks in the overlay editor.
  2. Edit the JSON manually or with another tool.
  3. Add delay, command, condition, or signal blocks between steps.
  4. Run the JSON through the launcher, runner, or a .command script.
  5. If the UI moved, use Edit Steps to rewrite only the recorded input while keeping the surrounding logic.

The editor records input order, not human timing. If you pause while thinking during recording, that pause is not stored.

JSON Model

A macro is an ordered top-level list of blocks. Most blocks are single actions in that list; repeat contains its own ordered blocks list for a fixed local loop.

{
  "version": 3,
  "hotkey": {
    "key": "F8",
    "modifiers": ["control", "option"]
  },
  "sourceScreen": { "width": 1512, "height": 982 },
  "loop": true,
  "blocks": [
    {
      "kind": "step",
      "name": "open field",
      "pace": 0.1,
      "actions": [
        { "kind": "move", "x": 0.5, "y": 0.5 },
        { "kind": "leftClick", "x": 0.5, "y": 0.5 },
        { "kind": "keyDown", "x": 0.5, "y": 0.5, "keyCode": 49, "modifiers": 0 },
        { "kind": "keyUp", "x": 0.5, "y": 0.5, "keyCode": 49, "modifiers": 0 }
      ]
    },
    { "kind": "condition", "command": "test -f ready.txt", "poll": 1, "timeout": 60 },
    { "kind": "command", "command": "cp ready.txt output.txt", "timeout": 10 },
    { "kind": "delay", "seconds": 1 },
    {
      "kind": "repeat",
      "count": 3,
      "blocks": [
        { "kind": "command", "command": "date >> ticks.log", "timeout": 10 },
        { "kind": "delay", "seconds": 0.5 }
      ]
    }
  ]
}

If loop is true, playback returns to block 0 after the final block. A pause before the next cycle should be modeled as the final delay.

hotkey is optional. It is used only by --hotkeys; normal editor and runner modes ignore it.

Blocks

step

Recorded input: cursor movement, mouse down/up, clicks, and key down/up actions.

Important fields:

  • actions - ordered input actions.
  • pace - synthetic seconds between actions. Default: 0.1.
  • compactMoves - if true, consecutive movement-only runs are compacted.

Playback begins with the first action. Include an initial move action when the cursor should move before the first click or key event.

Slow or animated UI often needs pace around 0.2 to 0.35.

delay

Fixed wait:

{ "kind": "delay", "seconds": 5 }

Use this only when fixed time is the right synchronization primitive.

command

Run one shell command once:

{ "kind": "command", "command": "cp input.txt output.txt", "timeout": 10 }

The macro continues only if the command exits with code 0. A non-zero exit stops playback.

condition

Poll a shell command until it exits with code 0:

{ "kind": "condition", "command": "test -s answer.txt", "poll": 1, "timeout": 120 }

Use condition when another system needs time to produce a file, text, state, or any shell-checkable result.

signal

Wait for a file to be created or modified:

{ "kind": "signal", "path": "signal.txt", "poll": 0.1 }

Macro snapshots file existence, size, and modification time on entry. It continues only after that state changes. Another process can release the macro with:

date +%s%N > signal.txt

repeat

Repeat one block or a sequence of blocks a fixed number of times:

{
  "kind": "repeat",
  "count": 5,
  "blocks": [
    {
      "kind": "step",
      "name": "click item",
      "pace": 0.15,
      "actions": [
        { "kind": "leftClick", "x": 0.5, "y": 0.5 }
      ]
    },
    { "kind": "delay", "seconds": 0.25 }
  ]
}

count is the number of local passes. blocks can contain any normal block, including another repeat. If count is 0, the block is skipped. Missing count is treated as 1.

Repeat And Handoff

There are two built-in repeat forms:

  • Repeat the whole macro forever with top-level "loop": true.
  • Repeat a local block sequence N times with a repeat block.

Example: run a recorded step, wait, and do that pair four times without looping the whole macro:

{
  "kind": "repeat",
  "count": 4,
  "blocks": [
    {
      "kind": "step",
      "name": "submit once",
      "pace": 0.2,
      "actions": [
        { "kind": "leftClick", "x": 0.5, "y": 0.5 }
      ]
    },
    { "kind": "condition", "command": "test -f ready-for-next.txt", "poll": 1, "timeout": 60 }
  ]
}

Use a final delay to pause between whole-macro cycles. Use signal or condition when a human, agent, app, or file-producing process decides when to continue. Use command and files for dynamic branching or counters that are more complex than fixed count.

Input Actions

Supported action.kind values inside a step:

  • move
  • leftClick
  • rightClick
  • leftDown
  • leftUp
  • rightDown
  • rightUp
  • keyDown
  • keyUp

Coordinates are normalized from 0 to 1 against the source screen. Playback maps them to the current main display. This helps with screen size differences, but it does not make workflows layout-independent.

For exact playback, keep the same:

  • main display
  • app/window placement
  • focused app
  • keyboard layout/input source
  • macOS Space/full-screen state
  • target app state

Overlay Behavior

Normal recording happens over the real desktop. The overlay ignores mouse events and uses a listen-only event tap, so input continues to reach the real applications. F2 clears only the currently recording step; already saved steps and non-step blocks stay unchanged.

Limitations

  • Main-display oriented.
  • Coordinate-based, not semantic UI automation.
  • No built-in image recognition or OCR.
  • No automatic recovery if a window, dialog, or loading state changes.
  • No per-action recorded timing.
  • Hotkey events are listen-only; choose shortcuts that do not conflict with the active app.
  • Runner errors currently go to system logs / Terminal output, not a rich run report.
  • Shell commands are trusted code.
  • macOS permissions can silently break capture or playback if not granted.

Macro works best for controlled, repeatable workflows where you can express readiness with files, shell checks, or explicit waits.

Git Hygiene

This repository ignores generated and personal files:

  • .build/
  • .DS_Store
  • *.log
  • Dialog.command
  • macro.config.json
  • tmp/
  • macros/*.json except macros/example.json

Do not commit personal macro JSON or local launcher scripts. Recorded macros may contain private screen coordinates, app workflows, file paths, prompt paths, clipboard logic, or destructive shell commands.

Development

Build:

swift build

Test:

swift test

Run launcher:

swift run macro

Run the safe committed example:

swift run macro -- --runner --play --macro example.json

Run a macro immediately:

swift run macro -- --runner --play --macro my-flow.json

Run the hotkey supervisor:

swift run macro -- --hotkeys

Regenerate the README demo GIF:

./scripts/render-demo-gif.command

⚡ Low-Level macOS CoreGraphics & HID Injection Architecture

MacroMac communicates directly with the macOS WindowServer via low-level CGEventCreateKeyboardEvent and CGEventTap APIs:

graph LR
    subgraph Macro Schema Parsing
        A[JSON Macro Definition] -->|Schema Validation & Guard Checks| B[Macro Execution Engine]
        B -->|Nested Frame Stack| C[Repeat & Loop Counter / Max 16 Depth]
    end

    subgraph CoreGraphics Event Dispatcher
        C -->|Raw Keycode + Modifier Mask| D[CGEventCreateKeyboardEvent]
        D -->|Atomic Down + 1.2ms Sleep + Up| E[CGEventPost to kCGHIDEventTap]
        E -->|Direct Mach Port Injection| F[Target Application Process / Window]
    end
Loading

⌨️ 1. Atomic Keystroke Injection & Modifier State Matrix

To prevent modifier keys (Command, Option, Control, Shift) from getting stuck down during high-speed macro bursts:

// Production C++/Objective-C++ CoreGraphics Event Dispatcher
void DispatchAtomicKeystroke(CGKeyCode keyCode, CGEventFlags flags, useconds_t holdDurationUs = 1200) {
    // 1. Create KeyDown event
    CGEventRef keyDown = CGEventCreateKeyboardEvent(nullptr, keyCode, true);
    CGEventSetFlags(keyDown, flags);
    
    // 2. Create KeyUp event
    CGEventRef keyUp = CGEventCreateKeyboardEvent(nullptr, keyCode, false);
    CGEventSetFlags(keyUp, flags);
    
    // 3. Post to system HID event tap
    CGEventPost(kCGHIDEventTap, keyDown);
    if (holdDurationUs > 0) {
        usleep(holdDurationUs); // 1.2ms hardware debounce simulation
    }
    CGEventPost(kCGHIDEventTap, keyUp);
    
    // 4. Guaranteed memory cleanup
    CFRelease(keyDown);
    CFRelease(keyUp);
}

🛡️ 2. Accessibility & Screen Recording Permissions Preflight

MacroMac requires zero-friction diagnostic preflight checks before starting the background daemon:

Required Entitlement Privacy Database Key Verification API Failure Behavior
Accessibility kTCCServiceAccessibility AXIsProcessTrustedWithOptions() Disables keystroke injection, prompts System Settings
Input Monitoring kTCCServiceListenEvent CGEventTapCreate() returns null Falls back to active window focus emulation
Screen Recording kTCCServiceScreenCapture CGPreflightScreenCaptureAccess() Optional: required only for pixel-color trigger conditions

📄 3. Production JSON Macro Recipe (Multi-Step Repeat Pipeline)

A real-world example executing an automated build-and-test sequence in Terminal with automated focus restoration:

{
  "name": "Quick Release Build & Test",
  "version": "1.0",
  "target_bundle_id": "com.apple.Terminal",
  "steps": [
    {
      "type": "command",
      "action": "focus_target_window"
    },
    {
      "type": "keystroke",
      "text": "cargo test --release\n",
      "delay_after_ms": 500
    },
    {
      "type": "repeat",
      "count": 5,
      "interval_ms": 250,
      "nested_steps": [
        { "type": "keystroke", "key": "down_arrow" }
      ]
    },
    {
      "type": "signal",
      "event": "build_completed_audio_chime"
    }
  ]
}

License

MIT. See LICENSE.

Non-goals

Macro intentionally stays small. More intelligent layers, such as OCR, image matching, LLM processing, or app-specific logic, should live outside the core and communicate through files, shell commands, condition, or signal.


👥 Синдикат Разработки

Разработано и поддерживается Жирняком и Адольфом Петушковым.

About

macOS CoreGraphics HID Keystroke & Mouse Automation Engine — Mach-port binary event injection, TCC security preflight & JSON macros.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages