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) - Titlemessage(string) - Message bodydetail(string, optional) - Long-form detail, shown in a scrollable monospaced text box — good for script output or error logsbuttons(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 titlefields(array) - Field definitionswidth(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 titlemessage(string, optional) - Prompt textdirectory(string, optional) - Starting directoryallowedTypes(array, optional) - Permitted file extensionsallowsMultiple(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 titlemessage(string, optional) - Prompt textdirectory(string, optional) - Starting directorynameFieldLabel(string, optional) - Label of the filename fieldnameFieldValue(string, optional) - Default filenameallowedTypes(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 titlemessage(string, optional) - Prompt textdirectory(string, optional) - Starting directoryallowsMultiple(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"}, ...}
- An array of strings:
opts(table, optional):title(string, optional) - Dialog titlemessage(string, optional) - Prompt textmultiSelect(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