Skip to content
MacScout

Pack API

The types and helpers an action pack works with, from ActionPack to ActionRunner.

Packs, their menus and the helpers on this page run on the main actor. ActionRunner.run and ActionRunner.shell start their process on a background thread and don't block the app while they wait. The work you hand to ActionCenter runs on the main actor too, so put slow Swift code in a Task.detached, as the built-in packs do.

ActionPack

@MainActor
protocol ActionPack {
    var id: String { get }
    var title: String { get }
    var icon: String { get }
    var summary: String { get }
    var isInline: Bool { get }
    func nodes(for context: ActionContext) -> [ActionNode]
}

Prop

Type

ActionContext

The selection a menu or shortcut works on.

Prop

Type

FileItem

A file or folder from a listing. The properties packs use most:

Prop

Type

MacScout adds filePath to URL: the plain path, without percent escapes and without a slash at the end. Use it when you pass a path to another program.

ActionNode

A menu entry. Build them with these three functions:

static func item(
    _ id: String,
    _ title: String,
    icon: String? = nil,
    shortcut: String? = nil,
    enabled: Bool = true,
    perform: @escaping @MainActor (ActionContext) -> Void
) -> ActionNode

case group(id: String, title: String, icon: String?, children: [ActionNode])

static func separator() -> ActionNode
  • item is an action. shortcut uses the format of custom action shortcuts, like "shift+cmd+e", and works whenever the item is in the menu for the current selection and enabled. enabled: false shows the item grayed out. perform gets the context at the moment the user picks the item.
  • group is a submenu, like Open in IDE.
  • separator() is a line between items. Only put it between two items, not at the start or end of a list.

ActionCenter

Runs work in the background and reports the result.

ActionCenter.shared.run(
    _ title: String,
    refresh pane: PaneModel? = nil,
    work: @escaping @MainActor () async throws -> ActionOutcome
)

While work runs, the corner of the window shows a progress indicator with the title. Afterwards, pane is reloaded, whether the work succeeded or not. What the user sees next depends on the result:

ResultShown
.doneA message: Title — done
.message(text)A message with your text
.output(text)A window titled after the action, with the text
a thrown errorThe error's message: a short one in the corner, one over 160 characters or with several lines in a window

For your own error messages, throw ActionError.failed("message").

ActionCenter.shared.toast(_ message: String, style:) shows a message on its own. The styles are .info, .success, .warning and .error. Warnings and errors stay for six seconds, the others for three.

ActionRunner

Runs programs and shell commands.

static func run(
    _ executable: String,
    _ arguments: [String],
    in directory: URL? = nil,
    environment: [String: String] = [:],
    input: Data? = nil
) async -> ProcessResult

static func shell(_ command: String, in directory: URL? = nil, environment: [String: String] = [:]) async -> ProcessResult

static func runInTerminal(_ command: String, in directory: URL)

static func which(_ tool: String) async -> String?
  • run starts a program without a shell and waits for it to exit. environment is added to MacScout's own environment. input is written to the program's standard input.
  • shell runs a command in the user's login shell, like the command of a custom action. Quote paths with ShellQuote.quote.
  • runInTerminal opens the user's terminal in directory and runs command there. It returns right away. In the App Store build, it opens the terminal and copies the command to the clipboard instead, see The App Store version.
  • which looks a tool up in the login shell's PATH and returns its path, or nil. Use it to check for a tool before offering an action that needs it.

ProcessResult has the exit status, stdout and stderr with surrounding whitespace removed, output with both combined, and succeeded for an exit status of 0.

Dialogs and output

ErrorPresenter.confirm(_ title: String, message: String, action: String, destructive: Bool = true) -> Bool
ErrorPresenter.show(_ error: Error, title: String)
ErrorPresenter.show(message: String, title: String)
OutputPanel.show(title: String, text: String)
  • confirm asks a question with a button labeled action and a Cancel button, and returns true if the user clicked the first one. Return confirms, Esc cancels. With destructive: true, the dialog shows a caution icon and the button is styled as destructive.
  • show reports an error in a dialog.
  • OutputPanel.show opens a window with monospaced text the user can select and copy.

Files and undo

FileOperations.uniqueURL(for name: String, in directory: URL, style: FileOperations.NamingStyle) -> URL
FileOperations.shared.recordCreated(_ urls: [URL])
FileOperations.shared.recordMoves(_ moves: [(from: URL, to: URL)])
  • uniqueURL returns a URL in directory that doesn't exist yet. With .numbered, Report.pdf stays Report.pdf if it's free, and becomes Report 2.pdf, Report 3.pdf and so on if it isn't. With .copy, the name is Report copy.pdf, then Report copy 2.pdf.
  • recordCreated lets ⌘Z move the new files to the Trash.
  • recordMoves lets ⌘Z move the files back.

To show new files right away, reload the pane and select them: context.pane?.reload(selecting: [url.filePath]).

Other helpers

HelperWhat it does
ShellQuote.quote(_:)Wraps a string in single quotes for the shell
SystemActions.open(_:with:)Opens URLs with a specific app
AppCatalog.editor, AppCatalog.terminalThe editor and terminal from the apps setting, or the ones MacScout found
AppCatalog.name(of:)The display name of an app, for titles like "Open in Zed"
ConfigStore.shared.configThe current contents of the config file
DiskAccess.isSandboxedtrue in the App Store build, which runs in the App Sandbox

On this page