ExMacOSControl.OSAScriptAdapter (ExMacOSControl v0.1.2)

View Source

Default adapter implementation using the osascript command-line tool.

This module implements the ExMacOSControl.Adapter behaviour and provides macOS automation functionality by executing AppleScript code, JavaScript for Automation (JXA) code, and Shortcuts via the osascript system command.

Features

  • Execute AppleScript code with timeout and argument support
  • Execute JXA code with timeout and argument support
  • Comprehensive error handling with ExMacOSControl.Error
  • Platform-independent timeout implementation using Task
  • Run macOS Shortcuts

Implementation Details

  • Uses System.cmd/3 to execute osascript with the provided script
  • Returns {:ok, output} on success (exit code 0)
  • Returns {:error, error} on failure with detailed error information
  • Trims whitespace from successful output
  • Supports timeout via Task.yield/2 and Task.shutdown/1
  • Supports both AppleScript (default) and JXA (-l JavaScript flag)
  • Arguments are passed directly to osascript (secure, no shell interpretation)

AppleScript Examples

# Basic execution
{:ok, result} = OSAScriptAdapter.run_applescript(~s(return "Hello"))
# => {:ok, "Hello"}

# With timeout
{:ok, result} = OSAScriptAdapter.run_applescript("delay 1", timeout: 5000)
# => {:ok, ""}

# With arguments
script = """
on run argv
  return item 1 of argv
end run
"""
{:ok, result} = OSAScriptAdapter.run_applescript(script, args: ["test"])
# => {:ok, "test"}

JXA Support

JavaScript for Automation (JXA) is Apple's JavaScript-based alternative to AppleScript. It provides the same automation capabilities but uses JavaScript syntax and semantics.

When to use JXA vs AppleScript

Use JXA when:

  • You're more comfortable with JavaScript than AppleScript
  • You need to leverage JavaScript's functional programming features
  • You want to use the ObjC bridge for direct Objective-C interaction
  • You're building complex data transformations

Use AppleScript when:

  • You're working with legacy scripts or examples
  • You need maximum compatibility (AppleScript is more widely documented)
  • The automation task is simple and straightforward

JXA Examples

# Basic JXA
{:ok, result} = run_javascript("(function() { return 'test'; })()")

# Application automation
{:ok, name} = run_javascript("Application('Finder').name()")

# With arguments
script = "function run(argv) { return argv[0]; }"
{:ok, result} = run_javascript(script, args: ["hello"])

# ObjC bridge
script = """
ObjC.import('Foundation');
var str = $.NSString.alloc.initWithUTF8String('test');
str.js;
"""
{:ok, result} = run_javascript(script)

Security Considerations

Arguments are passed directly to osascript without shell interpretation, making them safe from shell injection attacks. However, the AppleScript/JXA code itself should be from trusted sources as it executes with full system access.

Summary

Functions

Lists all available macOS Shortcuts.

Executes an AppleScript script without options.

Executes an AppleScript script with options.

Executes JavaScript for Automation (JXA) code using osascript.

Executes JavaScript for Automation (JXA) code with options.

Executes a script file from disk with automatic language detection.

Executes a macOS Shortcut by name without options.

Executes a macOS Shortcut by name with input parameters.

Functions

list_shortcuts()

@spec list_shortcuts() :: {:ok, [String.t()]} | {:error, term()}

Lists all available macOS Shortcuts.

Uses AppleScript to query the Shortcuts app for all shortcuts.

Returns

  • {:ok, shortcuts} - Success with list of shortcut names
  • {:error, error} - Failure (e.g., Shortcuts app not available)

Examples

OSAScriptAdapter.list_shortcuts()
# => {:ok, ["Shortcut 1", "Shortcut 2", "My Shortcut"]}

# If Shortcuts app is not available
# => {:error, error}

run_applescript(script)

@spec run_applescript(String.t()) ::
  {:ok, String.t()} | {:error, ExMacOSControl.Error.t()}

Executes an AppleScript script without options.

This is a convenience function that delegates to run_applescript/2 with an empty options list, maintaining backward compatibility.

Parameters

  • script - The AppleScript code to execute

Returns

  • {:ok, output} - On successful execution with script output
  • {:error, error} - On failure with detailed error information

Examples

iex> OSAScriptAdapter.run_applescript(~s(return "Hello, World!"))
{:ok, "Hello, World!"}

iex> OSAScriptAdapter.run_applescript("invalid script")
{:error, %ExMacOSControl.Error{type: :syntax_error, ...}}

run_applescript(script, opts)

@spec run_applescript(String.t(), ExMacOSControl.Adapter.options()) ::
  {:ok, String.t()} | {:error, ExMacOSControl.Error.t()}

Executes an AppleScript script with options.

Parameters

  • script - The AppleScript code to execute
  • opts - Keyword list of options:
    • :timeout - Maximum time in milliseconds to wait for execution
    • :args - List of string arguments to pass to the script

Returns

  • {:ok, output} - On successful execution with script output
  • {:error, error} - On failure with detailed error information

Timeout Behavior

When a timeout is specified, the script execution is monitored via a Task. If the script doesn't complete within the timeout period, it is terminated and a timeout error is returned.

Argument Passing

Arguments are passed to the AppleScript via the argv mechanism. Your AppleScript must use the on run argv handler to receive arguments.

Examples

# With timeout
script = "delay 2\nreturn \"done\""
OSAScriptAdapter.run_applescript(script, timeout: 5000)
# => {:ok, "done"}

# With arguments
script = """
on run argv
  return (item 1 of argv) & " " & (item 2 of argv)
end run
"""
OSAScriptAdapter.run_applescript(script, args: ["Hello", "World"])
# => {:ok, "Hello World"}

# Timeout exceeded
script = "delay 10"
OSAScriptAdapter.run_applescript(script, timeout: 100)
# => {:error, %ExMacOSControl.Error{type: :timeout, ...}}

# Multiple options
OSAScriptAdapter.run_applescript(script, timeout: 5000, args: ["test"])
# => {:ok, "test"}

run_javascript(script)

@spec run_javascript(String.t()) ::
  {:ok, String.t()} | {:error, ExMacOSControl.Error.t()}

Executes JavaScript for Automation (JXA) code using osascript.

This is a convenience wrapper around run_javascript/2 with no options.

Parameters

  • script - The JXA code to execute

Returns

  • {:ok, output} - Success with trimmed output
  • {:error, error} - Failure with detailed error information

Examples

iex> run_javascript("(function() { return 'test'; })()")
{:ok, "test"}

iex> run_javascript("Application('Finder').name()")
{:ok, "Finder"}

run_javascript(script, opts)

@spec run_javascript(String.t(), ExMacOSControl.Adapter.options()) ::
  {:ok, String.t()} | {:error, ExMacOSControl.Error.t()}

Executes JavaScript for Automation (JXA) code with options.

Uses osascript -l JavaScript to execute JXA code. Supports passing arguments to the script using the args option.

Parameters

  • script - The JXA code to execute
  • opts - Keyword list of options:
    • :args - List of string arguments to pass to the script (default: [])

Returns

  • {:ok, output} - Success with trimmed output
  • {:error, error} - Failure with detailed error information

Options

Arguments (:args)

Arguments are passed to the JXA script and available via the run(argv) function:

# JXA script receives arguments
function run(argv) {
  return argv[0];  // Returns first argument
}

Examples

# Basic execution
iex> run_javascript("(function() { return 'test'; })()", [])
{:ok, "test"}

# With arguments
iex> script = "function run(argv) { return argv[0]; }"
iex> run_javascript(script, args: ["hello"])
{:ok, "hello"}

# With multiple arguments
iex> script = "function run(argv) { return argv.join(' '); }"
iex> run_javascript(script, args: ["hello", "world"])
{:ok, "hello world"}

# System Events automation
iex> script = """
...> var app = Application('System Events');
...> var processes = app.processes.whose({ name: 'Finder' });
...> processes.length.toString();
...> """
iex> run_javascript(script, [])
{:ok, "1"}

run_script_file(file_path, opts)

@spec run_script_file(String.t(), ExMacOSControl.Adapter.options()) ::
  {:ok, String.t()} | {:error, ExMacOSControl.Error.t()}

Executes a script file from disk with automatic language detection.

This function executes AppleScript or JavaScript files directly using osascript, with automatic language detection based on file extension. It supports all the same options as run_applescript/2 and run_javascript/2, including timeout and argument passing.

Parameters

  • file_path - Absolute or relative path to the script file
  • opts - Keyword list of options:
    • :language - Explicit language (:applescript or :javascript), overrides detection
    • :timeout - Maximum time in milliseconds to wait for execution
    • :args - List of string arguments to pass to the script

Language Detection

The language is automatically detected from the file extension:

  • .scpt, .applescript → AppleScript
  • .js, .jxa → JavaScript

You can override automatic detection using the :language option.

File Validation

The function validates that:

  • The file exists
  • The path points to a regular file (not a directory)

Returns

  • {:ok, output} - On successful execution with script output
  • {:error, error} - On failure with detailed error information

Examples

# Execute AppleScript file with auto-detection
OSAScriptAdapter.run_script_file("/path/to/script.applescript")
# => {:ok, "result"}

# Execute JavaScript file with auto-detection
OSAScriptAdapter.run_script_file("/path/to/script.js")
# => {:ok, "result"}

# Override language detection
OSAScriptAdapter.run_script_file("/path/to/script.txt", language: :applescript)
# => {:ok, "result"}

# With arguments
OSAScriptAdapter.run_script_file(
  "/path/to/script.applescript",
  args: ["arg1", "arg2"]
)
# => {:ok, "result"}

# With timeout
OSAScriptAdapter.run_script_file("/path/to/script.js", timeout: 5000)
# => {:ok, "result"}

# Combined options
OSAScriptAdapter.run_script_file(
  "/path/to/script.scpt",
  language: :applescript,
  args: ["test"],
  timeout: 10_000
)
# => {:ok, "result"}

# File not found
OSAScriptAdapter.run_script_file("/nonexistent.scpt")
# => {:error, %ExMacOSControl.Error{type: :not_found, ...}}

run_shortcut(name)

@spec run_shortcut(String.t()) :: :ok | {:ok, String.t()} | {:error, term()}

Executes a macOS Shortcut by name without options.

This is a convenience function that delegates to run_shortcut/2 with an empty options list, maintaining backward compatibility.

Uses AppleScript to run the shortcut via Shortcuts Events.

Parameters

  • name - The name of the Shortcut to run

Returns

  • :ok - Success with no output
  • {:ok, output} - Success with output from the shortcut
  • {:error, error} - Failure with error reason

Examples

OSAScriptAdapter.run_shortcut("My Shortcut")
# => :ok (if shortcut exists and returns no output)
# => {:ok, "result"} (if shortcut returns output)
# => {:error, error} (if not found or error occurs)

run_shortcut(name, opts)

@spec run_shortcut(String.t(), ExMacOSControl.Adapter.options()) ::
  :ok | {:ok, String.t()} | {:error, term()}

Executes a macOS Shortcut by name with input parameters.

Uses AppleScript to run the shortcut via Shortcuts Events. Supports passing input data to the shortcut, which can be a string, number, map, or list.

Parameters

  • name - The name of the Shortcut to run
  • opts - Keyword list of options:
    • :input - Input data to pass to the shortcut (string, number, map, or list)

Returns

  • :ok - Success with no output
  • {:ok, output} - Success with output from the shortcut
  • {:error, error} - Failure with error reason

Input Types

The :input option supports various data types:

  • String: Passed directly as text
  • Number: Passed as a numeric value
  • Map: Serialized to JSON and passed as text
  • List: Serialized to JSON and passed as text

Examples

# Without input
OSAScriptAdapter.run_shortcut("My Shortcut")
# => :ok

# With string input
OSAScriptAdapter.run_shortcut("Process Text", input: "Hello, World!")
# => {:ok, "processed result"}

# With number input
OSAScriptAdapter.run_shortcut("Calculate", input: 42)
# => {:ok, "84"}

# With map input (serialized as JSON)
OSAScriptAdapter.run_shortcut("Process Data", input: %{"name" => "John", "age" => 30})
# => {:ok, "result"}

# With list input (serialized as JSON)
OSAScriptAdapter.run_shortcut("Process Items", input: ["item1", "item2", "item3"])
# => {:ok, "result"}