A Swift wrapper for bestline that makes it easy to build interactive command-line tools. This package gives you a fully-featured user prompt without the baggage of GNU readline.
- Line editing with emacs/vi key bindings
- History support with file persistence
- Tab completion with custom callbacks
- Syntax hints displayed inline
- Password mode for masked input
- Multi-line editing support
- Unicode support for diacritics, русский, Ελληνικά, 漢字, 仮名, 한글
Add this package to your Package.swift
dependencies:
dependencies: [
.package(url: "https://github.com/loopwork/bestline-swift.git", from: "1.0.0")
]
Then add Bestline
to your target dependencies:
targets: [
.target(
name: "YourTarget",
dependencies: ["Bestline"]
)
]
import Bestline
// Simple prompt
if let input = Bestline.readLine(prompt: "> ") {
print("You entered: \(input)")
}
// With initial text
if let input = Bestline.readLine(prompt: "> ", initialText: "Hello ") {
print("You entered: \(input)")
}
Tip
When running without proper terminal capabilities (e.g., in CI/CD pipelines, piped I/O, or unsupported terminals), bestline automatically falls back to basic input mode without interactive features.
Full Mode (all features available):
- Requires a real TTY (not piped input/output)
- Terminal type (
TERM
environment variable) must not bedumb
,cons25
, or >emacs
- Must support raw mode via termios
- Must support ANSI escape sequences
Fallback Mode (basic line reading only):
- Used when terminal capabilities are insufficient
- No line editing, completion, hints, or history navigation
- Simple
fgets()
-based input
History support lets the user recall previous commands with ↑ / ↓ arrow keys. Commands can be persisted to a file for access across sessions.
// Read with history file
let historyFile = "\(NSHomeDirectory())/.myapp_history"
if let input = Bestline.readLineWithHistory(prompt: "> ", historyFile: historyFile) {
print("You entered: \(input)")
Bestline.addToHistory(input)
}
// Manual history management
Bestline.loadHistory(from: historyFile)
Bestline.addToHistory("command 1")
Bestline.addToHistory("command 2")
Bestline.saveHistory(to: historyFile)
Tab completion provides automatic suggestions as users type, letting them to quickly complete commands, file names, or other inputs by pressing the Tab key.
Bestline.setCompletionCallback { input, position in
// Return completions based on current input
if input.hasPrefix("git ") {
return ["add", "commit", "push", "pull", "status", "branch"]
} else if input.hasPrefix("he") {
return ["hello", "help", "heap"]
}
return []
}
Syntax hints provide real-time contextual information as users type, displaying helpful suggestions or usage tips in muted gray text to the right of the cursor.
Bestline.setHintsCallback { input in
switch input {
case "git":
return " <command>"
case "help":
return " - Show help information"
case "exit":
return " - Exit the program"
default:
return nil
}
}
When handling sensitive input like passwords, you can enable mask mode to hide the characters as they're typed:
Bestline.enableMaskMode()
if let password = Bestline.readLine(prompt: "Password: ") {
// Terminal prints asterisks (*) instead of the actual characters typed
}
Bestline.disableMaskMode()
Multiline mode enables Ollama-style multiline input using triple quotes as delimiters. This is particularly useful for entering longer text, code snippets, or structured data.
// Enable multiline mode
Bestline.setMultilineMode(true)
When enabled, users can enter multiline content by:
- Typing
"""
to start multiline input - Entering text across multiple lines (pressing Enter creates new lines)
- Typing
"""
on its own line to end multiline input
Example interaction:
> """
... This is a multiline
... text input that spans
... multiple lines.
... """
The entire content between the triple quotes is returned as a single string with embedded newlines preserved.
Balance mode enables automatic bracket matching for parentheses, brackets, and braces. When enabled, bestline will visually highlight matching pairs as you type:
// Enable bracket matching
Bestline.setBalanceMode(true)
This helps when writing code or complex expressions by showing which brackets match. For example, when you type a closing bracket, the corresponding opening bracket will be briefly highlighted.
Emacs mode enables advanced keyboard shortcuts for line editing. This provides a familiar experience to users of readline, emacs, vi, and other editing software.
// Enable Emacs key bindings
Bestline.setEmacsMode(true)
In some cases, you may need to explicitly disable raw mode (useful for cleanup or when switching between different input modes):
// Disable raw mode
Bestline.disableRawMode()
This returns the terminal to its normal "cooked" mode, where input is line-buffered and special characters are processed by the terminal.
Here's a complete example of a command-line application using swift-argument-parser:
First, add the dependency to your Package.swift
:
dependencies: [
.package(url: "https://github.com/loopwork/swift-bestline.git", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.0.0")
]
Then create your executable:
import Foundation
import Bestline
import ArgumentParser
@main
struct MyREPL: ParsableCommand {
static let configuration = CommandConfiguration(
abstract: "An interactive REPL with enhanced line editing",
version: "1.0.0"
)
@Option(name: .shortAndLong, help: "History file location")
var historyFile: String?
@Option(name: .shortAndLong, help: "Custom prompt string")
var prompt: String = "repl> "
@Flag(name: .shortAndLong, help: "Enable verbose output")
var verbose: Bool = false
@Flag(help: "Disable history persistence")
var noHistory: Bool = false
func run() throws {
let historyPath = resolveHistoryFile()
if verbose {
print("Starting REPL with history file: \(historyPath)")
}
// Load history unless disabled
if !noHistory {
Bestline.loadHistory(from: historyPath)
}
setupCompletion()
setupHints()
print("Welcome to MyREPL. Type 'exit' to quit.")
if verbose {
print("History: \(noHistory ? "disabled" : "enabled")")
}
while true {
guard let line = Bestline.readLine(prompt: prompt) else {
// EOF (Ctrl-D)
break
}
let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.isEmpty { continue }
// Add to history unless disabled
if !noHistory {
Bestline.addToHistory(line)
}
// Process command
if !processCommand(trimmed) {
break // Exit requested
}
}
// Save history unless disabled
if !noHistory {
Bestline.saveHistory(to: historyFile)
}
print("\nGoodbye!")
}
private func resolveHistoryFile() -> String {
if let customPath = historyFile {
return customPath
}
let stateHome = ProcessInfo.processInfo.environment["XDG_STATE_HOME"]
?? "\(FileManager.default.homeDirectoryForCurrentUser.path)/.local/state"
return "\(stateHome)/myrepl_history"
}
private func setupCompletion() {
Bestline.setCompletionCallback { input, position in
let commands = ["exit", "clear", "echo", "version"]
return commands.filter { $0.hasPrefix(input) }
}
}
private func setupHints() {
Bestline.setHintsCallback { input in
switch input {
case "exit":
return " - Exit the REPL"
case "clear":
return " - Clear the screen"
case "version":
return " - Show version information"
default:
return nil
}
}
}
private func processCommand(_ command: String) -> Bool {
switch command {
case "exit":
return false
case "clear":
Bestline.clearScreen()
case "version":
print("MyREPL version \(Self.configuration.version ?? "unknown")")
default:
if command.hasPrefix("echo ") {
let text = String(command.dropFirst(5))
print(text)
} else {
print("Unknown command: \(command)")
print("Available: exit, clear, echo, version")
}
}
return true
}
}
This Swift wrapper is provided under the same 2-clause BSD license as the original bestline library. See the bestline repository for details.