Skip to content

Elixir wrapper library for macOS interaction through osascript and Shortcuts

License

Notifications You must be signed in to change notification settings

houllette/ex_macos_control

Repository files navigation

ExMacOSControl

Control your Mac with Elixir

hex.pm version hex.pm downloads hex.pm license Last Updated

A production-ready Elixir library for macOS automation via AppleScript and JavaScript for Automation (JXA). Automate Safari, Finder, Mail, Messages, and more—all with the safety and reliability of Elixir.

Why ExMacOSControl?

  • 🎯 Type-Safe Automation: Leverage Elixir's type system and pattern matching for robust automation
  • 🧪 Test-Friendly: Built-in adapter pattern with Mox support makes testing automation code straightforward
  • ⚡ Production-Ready: Automatic retry logic, telemetry integration, and comprehensive error handling
  • 📦 Batteries Included: Pre-built modules for Safari, Finder, Mail, Messages, and system control
  • 🔒 Permission Management: Built-in helpers for checking and requesting macOS permissions
  • 🛠️ Extensible: Clean patterns and guides for adding your own app modules

Use Cases

  • Developer Tooling: Automate your Mac-based development workflow
  • Testing & QA: Control Safari for browser testing, automate UI interactions
  • System Administration: Manage processes, files, and system settings programmatically
  • Communication Bots: Send automated emails and messages
  • Data Collection: Extract data from running applications
  • Productivity Automation: Build custom workflows combining multiple apps

Quick Example

Here's a complete workflow combining multiple features:

alias ExMacOSControl, as: Mac
alias ExMacOSControl.{Safari, Mail, Retry, Permissions}

# Check permissions before starting
case Permissions.check_automation("Safari") do
  {:ok, :granted} -> :ok
  {:ok, :not_granted} ->
    Permissions.show_automation_help("Safari")
    raise "Safari automation permission required"
end

# Scrape data from a website with automatic retry
{:ok, price} = Retry.with_retry(fn ->
  # Open URL
  :ok = Safari.open_url("https://example.com/product")
  Process.sleep(2000)  # Wait for page load

  # Extract price via JavaScript
  Safari.execute_javascript(~s|
    document.querySelector('.price').textContent
  |)
end, max_attempts: 3, backoff: :exponential)

# Send email notification if price dropped
if String.contains?(price, "$99") do
  :ok = Mail.send_email(
    to: "me@example.com",
    subject: "Price Alert!",
    body: "The product is now #{price}!"
  )

  IO.puts("✅ Alert sent!")
end

Features

Core Features

  • AppleScript Execution: Timeout support, argument passing, comprehensive error handling
  • JavaScript for Automation (JXA): Full JXA support with ObjC bridge access
  • Script File Execution: Auto-detect .applescript, .scpt, .js, .jxa files
  • macOS Shortcuts: Run Shortcuts with input parameters (strings, numbers, maps, lists)

App Modules

  • System Events: Process management, UI automation (menu clicks, keystrokes), file operations
  • Safari: Open URLs, execute JavaScript, manage tabs
  • Finder: Navigate folders, manage selections, set view modes
  • Mail: Send emails (with CC/BCC), search mailboxes, unread counts
  • Messages: Send iMessages/SMS, retrieve chats, unread counts

Advanced Features

  • Permissions: Check and manage macOS automation permissions
  • Retry Logic: Automatic retry with exponential/linear backoff
  • Telemetry: Built-in observability via :telemetry events
  • Script DSL: Optional Elixir DSL for building AppleScript
  • Platform Detection: Automatic macOS validation

Quick Start

AppleScript Execution

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

# With timeout (5 seconds)
{:ok, result} = ExMacOSControl.run_applescript("delay 2\nreturn \"done\"", timeout: 5000)
# => {:ok, "done"}

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

# Combined options
{:ok, result} = ExMacOSControl.run_applescript(script, timeout: 5000, args: ["Elixir"])
# => {:ok, "Hello, Elixir"}

JavaScript for Automation (JXA)

# Basic JXA execution
{:ok, result} = ExMacOSControl.run_javascript("(function() { return 'Hello from JXA!'; })()")
# => {:ok, "Hello from JXA!"}

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

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

Script File Execution

# Execute AppleScript file (auto-detected from .applescript extension)
{:ok, result} = ExMacOSControl.run_script_file("/path/to/script.applescript")

# Execute JavaScript file (auto-detected from .js extension)
{:ok, result} = ExMacOSControl.run_script_file("/path/to/script.js")

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

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

# Override language detection for files with non-standard extensions
{:ok, result} = ExMacOSControl.run_script_file(
  "/path/to/script.txt",
  language: :applescript
)

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

macOS Shortcuts

# Run macOS Shortcuts
:ok = ExMacOSControl.run_shortcut("My Shortcut Name")

# Run Shortcut with string input
{:ok, result} = ExMacOSControl.run_shortcut("Process Text", input: "Hello, World!")

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

# Run Shortcut with list input
{:ok, result} = ExMacOSControl.run_shortcut("Process Items", input: ["item1", "item2", "item3"])

# List available shortcuts
{:ok, shortcuts} = ExMacOSControl.list_shortcuts()
# => {:ok, ["Shortcut 1", "Shortcut 2", "My Shortcut"]}

# Check if a shortcut exists before running it
case ExMacOSControl.list_shortcuts() do
  {:ok, shortcuts} ->
    if "My Shortcut" in shortcuts do
      ExMacOSControl.run_shortcut("My Shortcut")
    end
  {:error, reason} ->
    {:error, reason}
end

System Events - Process Management

Control running applications on macOS:

# List all running apps
{:ok, processes} = ExMacOSControl.SystemEvents.list_processes()
# => {:ok, ["Safari", "Finder", "Terminal", "Mail", ...]}

# Check if an app is running
{:ok, true} = ExMacOSControl.SystemEvents.process_exists?("Safari")
# => {:ok, true}

{:ok, false} = ExMacOSControl.SystemEvents.process_exists?("NonexistentApp")
# => {:ok, false}

# Launch an app
:ok = ExMacOSControl.SystemEvents.launch_application("Calculator")
# => :ok

# Activate (bring to front) an app - same as launch
:ok = ExMacOSControl.SystemEvents.activate_application("Safari")
# => :ok

# Quit an app gracefully
:ok = ExMacOSControl.SystemEvents.quit_application("Calculator")
# => :ok

# Full workflow example
app_name = "Calculator"

# Check if it's running
case ExMacOSControl.SystemEvents.process_exists?(app_name) do
  {:ok, false} ->
    # Not running, launch it
    ExMacOSControl.SystemEvents.launch_application(app_name)
  {:ok, true} ->
    # Already running, bring to front
    ExMacOSControl.SystemEvents.activate_application(app_name)
end

Note: This module requires automation permission for System Events. macOS may prompt for permission on first use.

System Events - UI Automation

Control application UI elements programmatically (requires Accessibility permission):

# Click menu items
:ok = ExMacOSControl.SystemEvents.click_menu_item("Safari", "File", "New Tab")
# => :ok

:ok = ExMacOSControl.SystemEvents.click_menu_item("TextEdit", "Format", "Make Plain Text")
# => :ok

# Send keystrokes
:ok = ExMacOSControl.SystemEvents.press_key("TextEdit", "a")
# => :ok

# Send keystrokes with modifiers
:ok = ExMacOSControl.SystemEvents.press_key("Safari", "t", using: [:command])
# => :ok

# Multiple modifiers (Command+Shift+Q)
:ok = ExMacOSControl.SystemEvents.press_key("Safari", "q", using: [:command, :shift])
# => :ok

# Get window properties
{:ok, props} = ExMacOSControl.SystemEvents.get_window_properties("Safari")
# => {:ok, %{position: [100, 100], size: [800, 600], title: "Google"}}

# Application with no windows returns nil
{:ok, nil} = ExMacOSControl.SystemEvents.get_window_properties("AppWithNoWindows")
# => {:ok, nil}

# Set window bounds
:ok = ExMacOSControl.SystemEvents.set_window_bounds("Calculator",
  position: [100, 100],
  size: [400, 500]
)
# => :ok

# Complete UI automation workflow
# 1. Launch app
:ok = ExMacOSControl.SystemEvents.launch_application("TextEdit")

# 2. Create new document (Command+N)
:ok = ExMacOSControl.SystemEvents.press_key("TextEdit", "n", using: [:command])

# 3. Type some text
:ok = ExMacOSControl.SystemEvents.press_key("TextEdit", "H")
:ok = ExMacOSControl.SystemEvents.press_key("TextEdit", "e")
:ok = ExMacOSControl.SystemEvents.press_key("TextEdit", "l")
:ok = ExMacOSControl.SystemEvents.press_key("TextEdit", "l")
:ok = ExMacOSControl.SystemEvents.press_key("TextEdit", "o")

# 4. Get window properties
{:ok, props} = ExMacOSControl.SystemEvents.get_window_properties("TextEdit")

# 5. Resize window
:ok = ExMacOSControl.SystemEvents.set_window_bounds("TextEdit",
  position: [0, 0],
  size: [1000, 800]
)

Important: UI automation requires Accessibility permission. Enable in:

System Settings → Privacy & Security → Accessibility

(Or System Preferences → Security & Privacy → Privacy → Accessibility on older macOS)

Add Terminal (or your Elixir runtime) to the list of allowed applications.

Available Modifiers: :command, :control, :option, :shift

System Events - File Operations

Convenient helpers for file operations using Finder:

# Reveal file in Finder (opens window and selects the file)
:ok = ExMacOSControl.SystemEvents.reveal_in_finder("/Users/me/Documents/report.pdf")
# => :ok

# Get currently selected items in Finder
{:ok, selected} = ExMacOSControl.SystemEvents.get_selected_finder_items()
# => {:ok, ["/Users/me/file1.txt", "/Users/me/file2.txt"]}

# Empty selection returns empty list
{:ok, []} = ExMacOSControl.SystemEvents.get_selected_finder_items()
# => {:ok, []}

# Move file to trash
:ok = ExMacOSControl.SystemEvents.trash_file("/Users/me/old_file.txt")
# => :ok

# Complete workflow example
# 1. Create a test file
File.write!("/tmp/test.txt", "test content")

# 2. Reveal it in Finder
:ok = ExMacOSControl.SystemEvents.reveal_in_finder("/tmp/test.txt")

# 3. Get selected items (the file we just revealed should be selected)
{:ok, selected} = ExMacOSControl.SystemEvents.get_selected_finder_items()
# => {:ok, ["/tmp/test.txt"]}

# 4. Move to trash when done
:ok = ExMacOSControl.SystemEvents.trash_file("/tmp/test.txt")

# Error handling
{:error, error} = ExMacOSControl.SystemEvents.reveal_in_finder("/nonexistent/file")
# => {:error, %ExMacOSControl.Error{type: :not_found, ...}}

{:error, error} = ExMacOSControl.SystemEvents.trash_file("relative/path")
# => {:error, %ExMacOSControl.Error{type: :execution_error, message: "Path must be absolute", ...}}

Important Notes:

  • File operation paths must be absolute (start with /)
  • reveal_in_finder/1 will open a Finder window and bring Finder to the front
  • trash_file/1 moves items to Trash (not permanent deletion), but should still be used with caution
  • File operations require Finder access (usually granted automatically)

Finder Automation

Control the macOS Finder application:

# Get selected files in Finder
{:ok, files} = ExMacOSControl.Finder.get_selection()
# => {:ok, ["/Users/me/file.txt", "/Users/me/file2.txt"]}

# Empty selection returns empty list
{:ok, []} = ExMacOSControl.Finder.get_selection()
# => {:ok, []}

# Open Finder at a location
:ok = ExMacOSControl.Finder.open_location("/Users/me/Documents")
# => :ok

# Create new Finder window
:ok = ExMacOSControl.Finder.new_window("/Applications")
# => :ok

# Get current folder path
{:ok, path} = ExMacOSControl.Finder.get_current_folder()
# => {:ok, "/Users/me/Documents"}

# Returns empty string if no Finder windows open
{:ok, ""} = ExMacOSControl.Finder.get_current_folder()
# => {:ok, ""}

# Set view mode
:ok = ExMacOSControl.Finder.set_view(:icon)    # Icon view
:ok = ExMacOSControl.Finder.set_view(:list)    # List view
:ok = ExMacOSControl.Finder.set_view(:column)  # Column view
:ok = ExMacOSControl.Finder.set_view(:gallery) # Gallery view

# Error handling
{:error, error} = ExMacOSControl.Finder.open_location("/nonexistent/path")
# => {:error, %ExMacOSControl.Error{...}}

{:error, error} = ExMacOSControl.Finder.set_view(:invalid)
# => {:error, %ExMacOSControl.Error{type: :execution_error, message: "Invalid view mode", ...}}

Note: This module requires automation permission for Finder. macOS may prompt for permission on first use.

Safari Automation

Control Safari browser programmatically:

# Open URL in new tab
:ok = ExMacOSControl.Safari.open_url("https://example.com")
# => :ok

# Get current tab URL
{:ok, url} = ExMacOSControl.Safari.get_current_url()
# => {:ok, "https://example.com"}

# Execute JavaScript in current tab
{:ok, title} = ExMacOSControl.Safari.execute_javascript("document.title")
# => {:ok, "Example Domain"}

{:ok, result} = ExMacOSControl.Safari.execute_javascript("2 + 2")
# => {:ok, "4"}

# List all tab URLs across all windows
{:ok, urls} = ExMacOSControl.Safari.list_tabs()
# => {:ok, ["https://example.com", "https://google.com", "https://github.com"]}

# Close a tab by index (1-based)
:ok = ExMacOSControl.Safari.close_tab(2)
# => :ok

# Complete workflow example
# Open a new tab
:ok = ExMacOSControl.Safari.open_url("https://example.com")

# Wait for page to load, then execute JavaScript
Process.sleep(2000)
{:ok, title} = ExMacOSControl.Safari.execute_javascript("document.title")

# List all tabs
{:ok, tabs} = ExMacOSControl.Safari.list_tabs()
IO.inspect(tabs, label: "Open tabs")

# Close the first tab
:ok = ExMacOSControl.Safari.close_tab(1)

Note: This module requires automation permission for Safari. Tab indices are 1-based (1 is the first tab).

Mail Automation

Control Mail.app programmatically:

# Send an email
:ok = ExMacOSControl.Mail.send_email(
  to: "recipient@example.com",
  subject: "Automated Report",
  body: "Here is your daily report."
)

# Send with CC and BCC
:ok = ExMacOSControl.Mail.send_email(
  to: "team@example.com",
  subject: "Team Update",
  body: "Weekly status update.",
  cc: ["manager@example.com"],
  bcc: ["archive@example.com"]
)

# Get unread count (inbox)
{:ok, count} = ExMacOSControl.Mail.get_unread_count()
# => {:ok, 42}

# Get unread count (specific mailbox)
{:ok, count} = ExMacOSControl.Mail.get_unread_count("Work")
# => {:ok, 5}

# Search mailbox
{:ok, messages} = ExMacOSControl.Mail.search_mailbox("INBOX", "invoice")
# => {:ok, [%{subject: "Invoice #123", from: "billing@example.com", date: "2025-01-15"}, ...]}

# Complete workflow example
# Check unread count
{:ok, unread} = ExMacOSControl.Mail.get_unread_count()
IO.puts("You have #{unread} unread messages")

# Search for important messages
{:ok, messages} = ExMacOSControl.Mail.search_mailbox("INBOX", "urgent")

# Process search results
Enum.each(messages, fn msg ->
  IO.puts("From: #{msg.from}")
  IO.puts("Subject: #{msg.subject}")
  IO.puts("Date: #{msg.date}")
  IO.puts("---")
end)

# Send notification email if urgent messages found
if length(messages) > 0 do
  :ok = ExMacOSControl.Mail.send_email(
    to: "admin@example.com",
    subject: "Urgent Messages Alert",
    body: "Found #{length(messages)} urgent messages requiring attention."
  )
end

Important Safety Notes:

  • Mail automation requires Mail.app to be configured with an email account
  • send_email/1 sends emails immediately - there is no undo
  • Use with caution in production environments
  • Consider adding confirmation prompts before sending emails
  • Test with safe recipient addresses first

Messages Automation

⚠️ Safety Warning: Message sending functions will send real messages!

Control the Messages app programmatically:

# Send a message (iMessage or SMS)
:ok = ExMacOSControl.Messages.send_message("+1234567890", "Hello!")

# Send to a contact name
:ok = ExMacOSControl.Messages.send_message("John Doe", "Meeting at 3pm?")

# Force SMS (not iMessage)
:ok = ExMacOSControl.Messages.send_message(
  "+1234567890",
  "Hello!",
  service: :sms
)

# Force iMessage
:ok = ExMacOSControl.Messages.send_message(
  "john@icloud.com",
  "Hello!",
  service: :imessage
)

# Get recent messages from a chat
{:ok, messages} = ExMacOSControl.Messages.get_recent_messages("+1234567890")
# => {:ok, [
#   %{from: "+1234567890", text: "Hello!", timestamp: "Monday, January 15, 2024 at 2:30:00 PM"},
#   %{from: "+1234567890", text: "How are you?", timestamp: "Monday, January 15, 2024 at 2:31:00 PM"}
# ]}

# List all chats
{:ok, chats} = ExMacOSControl.Messages.list_chats()
# => {:ok, [
#   %{id: "iMessage;+E:+1234567890", name: "+1234567890", unread: 2},
#   %{id: "iMessage;-;+E:john@icloud.com", name: "John Doe", unread: 0}
# ]}

# Get total unread count
{:ok, count} = ExMacOSControl.Messages.get_unread_count()
# => {:ok, 5}

# Complete workflow example
# Check for unread messages
{:ok, unread} = ExMacOSControl.Messages.get_unread_count()

if unread > 0 do
  # List all chats to see who has unread messages
  {:ok, chats} = ExMacOSControl.Messages.list_chats()

  # Find chats with unread messages
  unread_chats = Enum.filter(chats, fn chat -> chat.unread > 0 end)

  # Get recent messages from the first unread chat
  if length(unread_chats) > 0 do
    first_chat = hd(unread_chats)
    {:ok, messages} = ExMacOSControl.Messages.get_recent_messages(first_chat.name)

    # Process the messages
    Enum.each(messages, fn msg ->
      IO.puts("From: #{msg.from}")
      IO.puts("Text: #{msg.text}")
      IO.puts("Time: #{msg.timestamp}")
      IO.puts("---")
    end)
  end
end

Required Permissions:

  • Automation permission for Terminal/your app to control Messages
  • Full Disk Access (for reading message history)

Important Safety Notes:

  • send_message/2 and send_message/3 send real messages immediately - there is no undo
  • Messages are sent via iMessage by default, falling back to SMS if iMessage is not available
  • Use the :service option to force SMS or iMessage
  • Be extremely careful when using in automated scripts
  • Consider adding confirmation prompts before sending messages
  • Test with your own phone number first

Checking and Managing Permissions

macOS requires explicit permissions for automation. Use the Permissions module to check and manage these:

# Check accessibility permission
case ExMacOSControl.Permissions.check_accessibility() do
  {:ok, :granted} ->
    IO.puts("Ready for UI automation!")
  {:ok, :not_granted} ->
    ExMacOSControl.Permissions.show_accessibility_help()
end

# Check automation permission for specific apps
ExMacOSControl.Permissions.check_automation("Safari")
# => {:ok, :granted} | {:ok, :not_granted}

# Get overview of all permissions
statuses = ExMacOSControl.Permissions.check_all()
# => %{accessibility: :granted, safari_automation: :not_granted, ...}

# Open System Settings to grant permissions
ExMacOSControl.Permissions.open_accessibility_preferences()
ExMacOSControl.Permissions.open_automation_preferences()

Required Permissions:

  • Accessibility: For UI automation (menu items, keystrokes, windows)
  • Automation: For controlling specific apps (Safari, Finder, Mail, etc.)
  • Full Disk Access: For some operations (e.g., Messages history)

Performance & Reliability

Retry Logic

ExMacOSControl includes automatic retry functionality for handling transient failures like timeouts:

alias ExMacOSControl.Retry

# Basic retry with exponential backoff (default: 3 attempts)
# Retries: immediately, after 200ms, after 400ms
{:ok, result} = Retry.with_retry(fn ->
  ExMacOSControl.Finder.get_selection()
end)

# Custom max attempts with linear backoff
# Retries: immediately, after 1s, after 1s, after 1s, after 1s
{:ok, windows} = Retry.with_retry(fn ->
  ExMacOSControl.SystemEvents.get_window_properties("Safari")
end, max_attempts: 5, backoff: :linear)

# Combining timeout and retry for reliability
{:ok, result} = Retry.with_retry(fn ->
  ExMacOSControl.run_applescript(script, timeout: 10_000)
end, max_attempts: 3, backoff: :exponential)

Retry Behavior:

  • Only retries timeout errors (errors with type: :timeout)
  • Non-timeout errors (syntax, permission, not found) return immediately
  • Exponential backoff: 200ms, 400ms, 800ms, 1600ms, etc.
  • Linear backoff: constant 1000ms between retries

When to Use:

  • ✅ Timeout errors that may succeed on retry
  • ✅ Operations depending on application state
  • ✅ UI automation affected by system responsiveness
  • ❌ Syntax errors (won't be fixed by retrying)
  • ❌ Permission errors (user intervention required)

Telemetry

ExMacOSControl emits telemetry events for monitoring and observability:

# In your application.ex
:telemetry.attach_many(
  "ex-macos-control-handler",
  [
    [:ex_macos_control, :applescript, :start],
    [:ex_macos_control, :applescript, :stop],
    [:ex_macos_control, :applescript, :exception],
    [:ex_macos_control, :retry, :start],
    [:ex_macos_control, :retry, :stop],
    [:ex_macos_control, :retry, :error]
  ],
  &MyApp.handle_telemetry/4,
  nil
)

# Example handler to track slow operations
defmodule MyApp do
  def handle_telemetry([:ex_macos_control, :applescript, :stop], measurements, metadata, _) do
    duration_ms = measurements.duration / 1_000

    if duration_ms > 5_000 do
      Logger.warning("Slow operation: #{duration_ms}ms - #{metadata.script}")
    end
  end

  def handle_telemetry(_, _, _, _), do: :ok
end

Available Events:

  • [:ex_macos_control, :applescript, :start] - Script execution begins
  • [:ex_macos_control, :applescript, :stop] - Script succeeds (includes duration in microseconds)
  • [:ex_macos_control, :applescript, :exception] - Script fails (includes error details)
  • [:ex_macos_control, :retry, :*] - Retry lifecycle events

See docs/performance.md for comprehensive performance guide including:

  • Timeout configuration recommendations
  • Common bottlenecks and solutions
  • Benchmarking strategies
  • When to use retry logic
  • Complete telemetry event reference

Installation

Add ex_macos_control to your list of dependencies in mix.exs:

def deps do
  [
    {:ex_macos_control, "~> 0.1.0"}
  ]
end

Then run:

mix deps.get

Verify Installation

# In iex
iex> ExMacOSControl.run_applescript(~s(return "Hello!"))
{:ok, "Hello!"}

If this works, you're ready to automate! 🎉

System Requirements

  • macOS 10.15 (Catalina) or later
  • Elixir 1.19 or later
  • Appropriate macOS permissions (accessibility, automation, etc.)

See the Permissions section for details on required permissions.

Documentation

Full documentation is available at https://hexdocs.pm/ex_macos_control.

Development

Setup

# Install dependencies
mix deps.get

# Install git hooks (recommended)
./scripts/install-hooks.sh

# Run tests
mix test

Git Hooks: This project uses pre-commit and pre-push hooks to ensure code quality. See docs/git_hooks.md for details.

Code Quality

This project uses strict code quality standards. All contributions must pass the following checks:

Run All Quality Checks

# Run all quality checks (format, credo, dialyzer)
mix quality

Individual Checks

# Format code
mix format

# Check code formatting
mix format.check

# Run Credo static analysis (strict mode)
mix credo --strict

# Run Dialyzer type checking
mix dialyzer

# Run tests
mix test

Quality Standards

  • Formatting: All code must be formatted with mix format (120 character line length)
  • Credo: All code must pass strict Credo checks with zero warnings
  • Dialyzer: All code must pass Dialyzer type checking with zero warnings
  • Tests: Aim for 100% test coverage on new code (minimum 90%)
  • Documentation: All public functions must have @doc, @spec, and @moduledoc

See CONTRIBUTING.md for detailed development guidelines and standards.

Extending ExMacOSControl

Creating New App Modules

Want to add automation for additional macOS apps? ExMacOSControl provides comprehensive documentation for creating new app automation modules.

See the App Module Creation Guide for:

  • Step-by-step instructions for creating modules
  • Common patterns and best practices
  • Testing strategies (unit and integration)
  • Complete examples (Music and Calendar modules)
  • Troubleshooting guide
  • Ready-to-use boilerplate templates

The guide includes everything you need to extend ExMacOSControl with new functionality while following established patterns and maintaining code quality standards.

What's Next?

📚 Learn More

💬 Get Help

🤝 Contributing

Contributions are welcome! Please see CONTRIBUTING.md for details on:

  • Code of conduct
  • Development setup
  • Testing requirements
  • Pull request process

Whether it's fixing bugs, adding new app modules, improving documentation, or sharing use cases—all contributions are appreciated!

License

This project is licensed under the MIT License - see the LICENSE file for details.

Acknowledgments

  • Built with Elixir
  • Powered by macOS osascript and the Shortcuts app
  • Inspired by the macOS automation community

About

Elixir wrapper library for macOS interaction through osascript and Shortcuts

Resources

License

Contributing

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •  

Languages