DEV Community

Khoa Pham
Khoa Pham

Posted on

How to present interactive CLI prompts in Swift

Swift is a great language for building command-line tools. Package managers, code generators, deployment scripts — they're all natural fits. But when it comes to collecting input from the user, the standard library leaves you with print and readLine, and not much else.

The result is usually something like this:

print("Enter your project name:")
let name = readLine() ?? ""
Enter fullscreen mode Exit fullscreen mode

It works, but there's no placeholder to guide the user, no inline validation feedback, no way to present a list of choices, and no graceful handling of Ctrl+C. For anything beyond the simplest script, you end up writing infrastructure instead of features.

Promptberry fills that gap. It's a Swift library for building interactive CLI prompts — text input, selects, confirmations, spinners, progress bars, and more.

The most common prompt is text. It supports a placeholder shown when the field is empty, and an optional validate closure that returns an error message if the input is invalid.

let name = try Promptberry.text(
    "Project name?",
    placeholder: "MyApp",
    validate: { $0.isEmpty ? "Name cannot be empty." : nil }
)
Enter fullscreen mode Exit fullscreen mode

Validation runs inline — the error appears immediately below the field without clearing what the user typed. For password fields, swap text for password and the input is masked automatically.

let secret = try Promptberry.password("Choose a password:", mask: "•")
Enter fullscreen mode Exit fullscreen mode

Selecting from a List

When the user needs to pick one option from a fixed set, select renders a navigable list. Arrow keys or j/k to move, Enter to confirm.

let projectType = try Promptberry.select(
    "Project type:",
    options: ["Executable", "Library", "Plugin"]
)
Enter fullscreen mode Exit fullscreen mode

If none of the built-in options apply, pass allowOther: true to let the user type a custom value instead.

For longer lists, autocomplete filters options as the user types, making it much faster to find the right entry.

let license = try Promptberry.autocomplete(
    "License:",
    options: ["MIT", "Apache 2.0", "GPL-3.0", "BSD-2-Clause", "MPL-2.0"],
    placeholder: "Type to filter..."
)
Enter fullscreen mode Exit fullscreen mode

When multiple selections are needed, multiselect lets the user toggle items with Space and confirm with Enter.

let features = try Promptberry.multiselect(
    "Include extras:",
    options: ["Tests", "CI workflow", "README", "SwiftLint"],
    initialValues: ["Tests", "README"]
)
Enter fullscreen mode Exit fullscreen mode

Confirmation

Before performing an irreversible action, always give the user a chance to back out.

let confirmed = try Promptberry.confirm(
    "Create \"\(name)\"?",
    active: "Yes, create it",
    inactive: "No, cancel"
)

guard confirmed else {
    Promptberry.cancel("Nothing created.")
    exit(0)
}
Enter fullscreen mode Exit fullscreen mode

Multi-line Input

For freeform input that spans multiple lines, multiline keeps the user in an editable buffer. Each Enter adds a new line, and Ctrl+D submits.

let description = try Promptberry.multiline(
    "Short description:",
    placeholder: "What does this project do?"
)
Enter fullscreen mode Exit fullscreen mode

Async Work with Spinners and Progress

Once the user has confirmed, the real work begins. For a sequence of async steps, tasks runs each one in order with its own animated spinner.

try await tasks([
    PromptTask("Scaffolding project structure") { try await scaffold() },
    PromptTask("Writing Package.swift") { try await writeManifest() },
    PromptTask("Installing dependencies") { try await installDeps() },
])
Enter fullscreen mode Exit fullscreen mode

If a task throws, its spinner switches to an error indicator and the sequence stops. For operations where you know the total number of steps, a progress bar communicates more than a spinner.

let p = Promptberry.progress(total: files.count, message: "Copying files...")
for file in files {
    try await copy(file)
    await p.advance()
}
await p.complete("All files copied!")
Enter fullscreen mode Exit fullscreen mode

Handling Cancellation

Every prompt throws PromptCancelled when the user presses Ctrl+C. Wrap your prompts in a do/catch to handle it without crashing.

do {
    let name = try Promptberry.text("Project name?")
    // ...
} catch is PromptCancelled {
    Promptberry.cancel("Cancelled.")
    exit(0)
}
Enter fullscreen mode Exit fullscreen mode

Framing the Session

intro and outro add a clear start and end to the interaction, making the tool feel deliberate rather than abrupt.

Promptberry.intro("New Swift Project")
// prompts...
Promptberry.outro("Happy coding!")
Enter fullscreen mode Exit fullscreen mode

Top comments (0)