# ExMacOSControl
**Control your Mac with Elixir**
[](https://hex.pm/packages/ex_macos_control)
[](https://hex.pm/packages/ex_macos_control)
[](https://github.com/houllette/ex_macos_control/blob/main/LICENSE)
[](https://github.com/houllette/ex_macos_control/commits/master)
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:
```elixir
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
```elixir
# 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)
```elixir
# 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
```elixir
# 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
```elixir
# 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:
```elixir
# 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):
```elixir
# 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:
```elixir
# 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:
```elixir
# 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:
```elixir
# 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:
```elixir
# 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:
```elixir
# 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:
```elixir
# 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:
```elixir
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:
```elixir
# 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](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`:
```elixir
def deps do
[
{:ex_macos_control, "~> 0.1.0"}
]
end
```
Then run:
```bash
mix deps.get
```
### Verify Installation
```elixir
# 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](#checking-and-managing-permissions) for details on required permissions.
## Documentation
Full documentation is available at [https://hexdocs.pm/ex_macos_control](https://hexdocs.pm/ex_macos_control).
## Development
### Setup
```bash
# 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](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
```bash
# Run all quality checks (format, credo, dialyzer)
mix quality
```
#### Individual Checks
```bash
# 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](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](docs/creating_app_modules.md) 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
- **[Getting Started Guide](https://hexdocs.pm/ex_macos_control/guides_getting_started.html)** - Complete walkthrough for first-time users
- **[Common Patterns](https://hexdocs.pm/ex_macos_control/guides_common_patterns.html)** - Real-world automation examples and workflows
- **[DSL vs Raw AppleScript](https://hexdocs.pm/ex_macos_control/guides_dsl_vs_raw.html)** - Choosing the right approach for your use case
- **[Advanced Usage](https://hexdocs.pm/ex_macos_control/guides_advanced_usage.html)** - Telemetry, custom adapters, and performance tuning
- **[Performance Guide](https://hexdocs.pm/ex_macos_control/performance.html)** - Optimization tips and best practices
### π¬ Get Help
- **Issues**: [GitHub Issues](https://github.com/houllette/ex_macos_control/issues) for bugs and feature requests
- **Discussions**: [GitHub Discussions](https://github.com/houllette/ex_macos_control/discussions) for questions and community support
- **Documentation**: [HexDocs](https://hexdocs.pm/ex_macos_control) for API reference
### π€ Contributing
Contributions are welcome! Please see [CONTRIBUTING.md](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](LICENSE) file for details.
## Acknowledgments
- Built with [Elixir](https://elixir-lang.org/)
- Powered by macOS [osascript](https://ss64.com/osx/osascript.html) and the Shortcuts app
- Inspired by the macOS automation community