app.progress - Progress Dialog API

Show and drive progress dialogs.

Permission: L0 Safe — no declaration needed, always available

The module has a single entry point: app.progress.create(config) shows a dialog and returns a handle; everything after that is a method on the handle.

local p = app.progress.create({ title = "Working" })
p:update(50, "Halfway there")
p:close()

Handle methods are called with a colon (p:update(...)), not a dot.

app.progress.create(config)

Creates and shows a progress dialog.

Parameters:

  • config (table):
    • title (string, required) - Dialog title
    • message (string) - Detail message, empty by default
    • indeterminate (boolean) - Indeterminate progress (default true, a sliding bar)
    • cancellable (boolean) - Show a Cancel button (default false)
    • onCancel (function) - Called when the user clicks Cancel

Returns: progressHandle; returns nil, error on failure

local p, err = app.progress.create({
    title = "Downloading",
    message = "Preparing...",
    indeterminate = false,
    cancellable = true,
    onCancel = function()
        app.log.info("Download cancelled")
    end
})
if not p then
    app.log.error("Could not create progress dialog: " .. tostring(err))
    return
end

Omitting title fails outright — it is the only required field.

Handle methods

handle:update(value, message?)

Updates the progress value and optionally the message.

Parameters:

  • value (number) - Progress, 0–100 (not 0–1)
  • message (string, optional) - Also update the message text
for i = 1, #files do
    p:update(i * 100 / #files, "Processing: " .. app.path.basename(files[i]))
end

Calling update() switches an indeterminate dialog to determinate. Even when created with indeterminate = true, the first update() turns the sliding bar into a filled bar driven by value. To stay indeterminate, use only setMessage().

handle:setMessage(message)

Updates only the message text. It does not touch the progress value and does not switch an indeterminate dialog to determinate.

Parameters:

  • message (string) - The new message
p:setMessage("Running step 2...")

handle:isCancelled()

Whether the user has clicked Cancel. Requires cancellable = true at creation.

Returns: boolean

if p:isCancelled() then
    p:close()
    return
end

handle:close()

Closes the dialog.

p:close()

Two ways to handle cancellation

With cancellable = true there are two approaches; pick one based on the shape of your work:

Poll isCancelled() — good for batch loops, where the exit point is visible:

for i = 1, 100 do
    if p:isCancelled() then break end
    p:update(i, "Step " .. i)
end

The onCancel callback — good when there is no loop to check inside (waiting on an async result, for example):

local cancelled = false
local p = app.progress.create({
    title = "Long operation",
    cancellable = true,
    onCancel = function() cancelled = true end
})

They can be combined, but there is rarely a reason to.

Examples

Batch file processing

function MyPlugin:handleBatchProcess(context)
    local files = context.selectedFiles
    local total = #files

    local p = app.progress.create({
        title = "Batch processing",
        message = "Preparing...",
        indeterminate = false,
        cancellable = true
    })
    if not p then return end

    local processed = 0
    for i, file in ipairs(files) do
        if p:isCancelled() then break end

        p:update(i * 100 / total, "Processing: " .. app.path.basename(file))
        self:processFile(file)
        processed = processed + 1
    end

    p:close()
    app.notification.show("Done", "Processed " .. processed .. " files")
end

Indeterminate progress (unknown duration)

function MyPlugin:handleSearch(context)
    local p = app.progress.create({
        title = "Searching",
        message = "Looking through files...",
        indeterminate = true
    })
    if not p then return end

    -- Only setMessage here, never update — update would turn the bar solid
    p:setMessage("Scanning subdirectories...")
    local results = self:searchFiles()

    p:close()
    app.notification.show("Done", "Found " .. #results .. " files")
end

Several at once

Every create() is an independent dialog; they do not interfere:

function MyPlugin:handleParallelDownload(context)
    local urls = {"url1", "url2", "url3"}
    local bars = {}

    for i, url in ipairs(urls) do
        bars[i] = app.progress.create({
            title = "Download " .. i,
            message = "Preparing...",
            indeterminate = false
        })
    end

    for pct = 1, 100 do
        for _, p in ipairs(bars) do
            if p then p:update(pct, "Downloading... " .. pct .. "%") end
        end
    end

    for _, p in ipairs(bars) do
        if p then p:close() end
    end
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