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() -> ActionNodeitemis an action.shortcutuses 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: falseshows the item grayed out.performgets the context at the moment the user picks the item.groupis 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:
| Result | Shown |
|---|---|
.done | A message: Title — done |
.message(text) | A message with your text |
.output(text) | A window titled after the action, with the text |
| a thrown error | The 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?runstarts a program without a shell and waits for it to exit.environmentis added to MacScout's own environment.inputis written to the program's standard input.shellruns a command in the user's login shell, like the command of a custom action. Quote paths withShellQuote.quote.runInTerminalopens the user's terminal indirectoryand runscommandthere. 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.whichlooks a tool up in the login shell'sPATHand returns its path, ornil. 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)confirmasks a question with a button labeledactionand a Cancel button, and returnstrueif the user clicked the first one. Return confirms, Esc cancels. Withdestructive: true, the dialog shows a caution icon and the button is styled as destructive.showreports an error in a dialog.OutputPanel.showopens 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)])uniqueURLreturns a URL indirectorythat doesn't exist yet. With.numbered,Report.pdfstaysReport.pdfif it's free, and becomesReport 2.pdf,Report 3.pdfand so on if it isn't. With.copy, the name isReport copy.pdf, thenReport copy 2.pdf.recordCreatedlets ⌘Z move the new files to the Trash.recordMoveslets ⌘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
| Helper | What 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.terminal | The 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.config | The current contents of the config file |
DiskAccess.isSandboxed | true in the App Store build, which runs in the App Sandbox |