README.md

# ExMacOSControl

**Control your Mac with Elixir**

[![hex.pm version](https://img.shields.io/hexpm/v/ex_macos_control.svg)](https://hex.pm/packages/ex_macos_control)
[![hex.pm downloads](https://img.shields.io/hexpm/dt/ex_macos_control.svg)](https://hex.pm/packages/ex_macos_control)
[![hex.pm license](https://img.shields.io/hexpm/l/ex_macos_control.svg)](https://github.com/houllette/ex_macos_control/blob/main/LICENSE)
[![Last Updated](https://img.shields.io/github/last-commit/houllette/ex_macos_control.svg)](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