app.dialog - Dialog API

Dialogs and forms.

Permission: L0 Safe — always available, no declaration needed

Message dialogs

app.dialog.alert(config)

Shows a message dialog.

Parameters:

  • config (table):
    • title (string) - Title
    • message (string) - Message body
    • detail (string, optional) - Long-form detail, shown in a scrollable monospaced text box — good for script output or error logs
    • buttons (array, optional) - Button titles (default ["OK"])
    • style (string, optional) - "info" (default), "warning" or "critical"

Returns: number - Index of the button that was clicked, starting at 1

Buttons run right to left, and the first one is the default. macOS puts the button you add first at the far right and makes it the default (highlighted, triggered by Return). So the primary action goes first and Cancel last:

buttons = {"Move to Trash", "Cancel"}   -- rightmost and default; returns 1

Reversing them makes Cancel the default, so pressing Return cancels.

-- A simple message
app.dialog.alert({title = "Notice", message = "Done"})

-- With several buttons
local result = app.dialog.alert({
    title = "Confirm",
    message = "Do you want to continue?",
    buttons = {"Continue", "Cancel"},
    style = "warning"
})

if result == 1 then
    -- "Continue" was clicked
end

-- Long detail text, shown in a scrollable box
local result = app.shell.execute("git log --oneline -20")
app.dialog.alert({
    title = "Git log",
    message = "The last 20 commits:",
    detail = result
})

Form dialogs

app.dialog.form(config)

Shows a form dialog to collect input.

Parameters:

  • config (table):
    • title (string) - Form title
    • fields (array) - Field definitions
    • width (number, optional) - Width (default 500)
    • maxHeight (number, optional) - Maximum height (default 500)
    • onLoad (function, optional) - Called once the form is up (non-blocking mode)
    • onSubmit (function, optional) - Called on submit (non-blocking mode)
    • onCancel (function, optional) - Called on cancel (non-blocking mode)

Returns:

  • Blocking mode: table|nil — the field values, or nil if cancelled
  • Non-blocking mode: formHandle — a handle to the live form

Blocking mode (no callbacks):

local result = app.dialog.form({
    title = "Settings",
    width = 400,
    fields = {
        {type = "text", id = "name", label = "Name", default = ""},
        {type = "number", id = "count", label = "Count", default = 10},
        {type = "select", id = "format", label = "Format",
         options = {"PNG", "JPEG", "WebP"}, default = "PNG"},
        {type = "checkbox", id = "optimize", label = "Optimize", default = true}
    }
})

if result then
    print(result.name)      -- string
    print(result.count)     -- number
    print(result.format)    -- string
    print(result.optimize)  -- boolean
end

Non-blocking mode (with callbacks):

app.dialog.form({
    title = "Choose a certificate",
    fields = {
        {type = "select", id = "cert", label = "Certificate",
         options = {}, loading = true}
    },
    onLoad = function(form)
        -- Load the data asynchronously
        app.thread.create(function()
            local certs = loadCertificates()
            form:setFieldOptions("cert", certs, certs[1])
            form:setFieldLoading("cert", false)
        end)
    end,
    onSubmit = function(values)
        print("Chose: " .. values.cert)
    end,
    onCancel = function()
        print("Cancelled")
    end
})

Form handle methods (non-blocking mode):

Method What it does
form:updateField(id, props) Updates field properties — the general form, described below
form:setFieldValue(id, value) Sets the field’s value
form:setFieldOptions(id, options, default?) Sets the options of a select
form:setFieldLoading(id, bool) Shows or hides a loading spinner
form:setFieldEnabled(id, bool) Enables or disables the field
form:setFieldPlaceholder(id, text) Sets the placeholder text
form:close() Closes the form

form:updateField(id, props) — the general updater; it can change several properties at once:

-- Change several properties in one call
form:updateField("cert", {
    options = {"Cert A", "Cert B"},
    default = "Cert A",
    loading = false,
    enabled = true
})

-- Equivalent to calling each setter separately:
-- form:setFieldOptions("cert", {"Cert A", "Cert B"}, "Cert A")
-- form:setFieldLoading("cert", false)
-- form:setFieldEnabled("cert", true)

Supported properties: value, options, default, loading, enabled, placeholder.

File dialogs

app.dialog.openFile(config?)

Shows an Open panel.

Parameters:

  • config (table, optional):
    • title (string, optional) - Dialog title
    • message (string, optional) - Prompt text
    • directory (string, optional) - Starting directory
    • allowedTypes (array, optional) - Permitted file extensions
    • allowsMultiple (boolean, optional) - Allow selecting more than one (default false)

Returns: string|array|nil - The path(s), or nil if cancelled

local path = app.dialog.openFile({
    title = "Choose an image",
    directory = "~/Pictures",
    allowedTypes = {"png", "jpg", "gif"},
    allowsMultiple = false
})

-- Multiple selection
local paths = app.dialog.openFile({
    allowsMultiple = true
})

app.dialog.saveFile(config?)

Shows a Save panel.

Parameters:

  • config (table, optional):
    • title (string, optional) - Dialog title
    • message (string, optional) - Prompt text
    • directory (string, optional) - Starting directory
    • nameFieldLabel (string, optional) - Label of the filename field
    • nameFieldValue (string, optional) - Default filename
    • allowedTypes (array, optional) - Permitted file extensions

Returns: string|nil - The path, or nil if cancelled

local path = app.dialog.saveFile({
    title = "Save As",
    nameFieldValue = "output.png"
})

app.dialog.chooseFolder(config?)

Shows a folder chooser.

Parameters:

  • config (table, optional):
    • title (string, optional) - Dialog title
    • message (string, optional) - Prompt text
    • directory (string, optional) - Starting directory
    • allowsMultiple (boolean, optional) - Allow selecting more than one (default false)

Returns: string|array|nil - The path(s), or nil if cancelled

local folder = app.dialog.chooseFolder({
    title = "Choose an output folder"
})

List chooser

app.dialog.choose(items, opts?)

Shows a list for the user to pick from.

Parameters:

  • items (array) - The items, in either of two shapes:
    • An array of strings: {"Option A", "Option B", "Option C"}
    • An array of tables: {{id = "a", text = "Option A", icon = "sf:star"}, ...}
  • opts (table, optional):
    • title (string, optional) - Dialog title
    • message (string, optional) - Prompt text
    • multiSelect (boolean, optional) - Allow multiple selection (default false)
    • defaultIndex (number, optional) - Index selected initially (0-based, default 0)

Returns:

  • Single selection: the chosen item (string or table), or nil if cancelled
  • Multiple selection: an array of chosen items, or nil if cancelled
-- A list of strings, single selection
local selected = app.dialog.choose(
    {"PNG", "JPEG", "WebP", "TIFF"},
    {title = "Format", message = "Choose an export format"}
)
if selected then
    app.log.info("Chose: " .. selected)
end

-- A list of tables
local selected = app.dialog.choose({
    {id = "resize", text = "Resize", icon = "sf:arrow.up.left.and.arrow.down.right"},
    {id = "crop",   text = "Crop",     icon = "sf:crop"},
    {id = "rotate", text = "Rotate",   icon = "sf:rotate.right"},
}, {title = "Choose an action"})

if selected then
    app.log.info("Chose: " .. selected.id)
end

-- Multiple selection
local selected = app.dialog.choose(
    {"Tag A", "Tag B", "Tag C", "Tag D"},
    {title = "Choose tags", multiSelect = true}
)
if selected then
    app.log.info("Chose " .. #selected .. " tags")
end

Examples

Batch rename

function MyPlugin:handleRename(context)
    local result = app.dialog.form({
        title = "Batch rename",
        fields = {
            {type = "text", id = "prefix", label = "Prefix", default = ""},
            {type = "text", id = "suffix", label = "Suffix", default = ""},
            {type = "number", id = "start", label = "Start number", default = 1}
        }
    })

    if not result then return end

    local num = result.start
    for _, file in ipairs(context.selectedFiles) do
        local dir = app.path.dirname(file)
        local ext = app.path.extension(file)
        local newName = string.format("%s%03d%s.%s",
            result.prefix, num, result.suffix, ext)
        local newPath = app.path.join(dir, newName)

        app.file.move(file, newPath)
        num = num + 1
    end

    app.notification.show("Done", "Renamed " .. #context.selectedFiles .. " files")
end
Developer Documentation
User Guide
Getting Started Menu Editor Guide Plugin Manager Settings Script Menus FAQ
Script Development
Development Guide
Plugin Development
Quick Start Development Guide Example Plugins
API Reference
Overview API Query Plugin Info Logging Finder Context Plugin Settings Internationalization
UI & Interaction
Dialog Progress Notification Chooser WebView Status Bar Dock
Files & Paths
File Operations Path Utilities Finder Actions Trash Extended Attributes Metadata File Watcher
Data Formats
JSON Plist CSV XML PDF Image
Text & Encoding
String Regex Date & Time Color Crypto
System
Shell Commands Process Application System Info AppleScript Shortcuts
System Info
Network Power/Battery Screen/Appearance Audio Bluetooth Location
Network
HTTP WebSocket URL
Input & Clipboard
Keyboard Mouse Hotkey Clipboard Window
Storage
SQLite Keychain UserDefaults
Media
OCR QR Code
Utilities
Archive UTI Share Timer Wake Lock Thread