zig-utils/zig-cli
A modern, feature-rich CLI library for Zig.
A modern, feature-rich CLI library for Zig, inspired by popular frameworks like clapp. Build beautiful command-line applications and interactive prompts with ease.
.red().bold().underline())Add zig-cli to your build.zig:
const zig_cli = b.dependency("zig-cli", .{
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("zig-cli", zig_cli.module("zig-cli"));
const std = @import("std");
const cli = @import("zig-cli");
fn greetAction(ctx: *cli.Command.ParseContext) !void {
const name = ctx.getOption("name") orelse "World";
const stdout = std.io.getStdOut().writer();
try stdout.print("Hello, {s}!\n", .{name});
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Create CLI application
var app = try cli.CLI.init(
allocator,
"myapp",
"1.0.0",
"My awesome CLI application"
);
defer app.deinit();
// Add options
const name_option = cli.Option.init("name", "name", "Your name", .string)
.withShort('n')
.withDefault("World");
_ = try app.option(name_option);
// Set action
_ = app.action(greetAction);
// Parse arguments
const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
try app.parse(args);
}
const std = @import("std");
const prompt = @import("zig-cli").prompt;
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Text prompt
var text_prompt = prompt.TextPrompt.init(allocator, "What is your name?");
defer text_prompt.deinit();
const name = try text_prompt.prompt();
defer allocator.free(name);
// Confirm prompt
var confirm_prompt = prompt.ConfirmPrompt.init(allocator, "Continue?");
defer confirm_prompt.deinit();
const confirmed = try confirm_prompt.prompt();
// Select prompt
const choices = [_]prompt.SelectPrompt.Choice{
.{ .label = "Option 1", .value = "opt1" },
.{ .label = "Option 2", .value = "opt2" },
};
var select_prompt = prompt.SelectPrompt.init(allocator, "Choose:", &choices);
defer select_prompt.deinit();
const selected = try select_prompt.prompt();
defer allocator.free(selected);
}
var app = try cli.CLI.init(allocator, "app-name", "1.0.0", "Description");
defer app.deinit();
const option = cli.Option.init("name", "long-name", "Description", .string)
.withShort('n') // Short flag (-n)
.withRequired(true) // Make it required
.withDefault("value"); // Set default value
_ = try app.option(option);
Option types:
.string - String value.int - Integer value.float - Float value.bool - Boolean flagconst arg = cli.Argument.init("name", "Description", .string)
.withRequired(true) // Required argument
.withVariadic(false); // Accept multiple values
_ = try app.argument(arg);
const subcmd = try cli.Command.init(allocator, "subcmd", "Subcommand description");
// Add aliases for the command
_ = try subcmd.addAlias("sub");
_ = try subcmd.addAlias("s");
const opt = cli.Option.init("opt", "option", "Option description", .string);
_ = try subcmd.addOption(opt);
_ = subcmd.setAction(myAction);
_ = try app.command(subcmd);
Now you can call the subcommand with: myapp subcmd, myapp sub, or myapp s
Add pre/post command hooks to your CLI:
var chain = cli.Middleware.MiddlewareChain.init(allocator);
defer chain.deinit();
// Add built-in middleware
try chain.use(cli.Middleware.Middleware.init("logging", cli.Middleware.loggingMiddleware));
try chain.use(cli.Middleware.Middleware.init("timing", cli.Middleware.timingMiddleware));
try chain.use(cli.Middleware.Middleware.init("validation", cli.Middleware.validationMiddleware));
// Custom middleware
fn authMiddleware(ctx: *cli.Middleware.MiddlewareContext) !bool {
const is_authenticated = checkAuth();
if (!is_authenticated) {
try ctx.set("error", "Unauthorized");
return false; // Stop chain
}
try ctx.set("user", "[email protected]");
return true; // Continue
}
// Add with priority (lower runs first)
try chain.use(cli.Middleware.Middleware.init("auth", authMiddleware).withOrder(-10));
// Execute middleware chain before command
var middleware_ctx = cli.Middleware.MiddlewareContext.init(allocator, parse_context, command);
defer middleware_ctx.deinit();
if (try chain.execute(&middleware_ctx)) {
// All middleware passed, execute command
try command.executeAction(parse_context);
}
Built-in middleware:
loggingMiddleware - Logs command executiontimingMiddleware - Records start timevalidationMiddleware - Validates required optionsenvironmentCheckMiddleware - Checks environment variablesfn myAction(ctx: *cli.Command.ParseContext) !void {
// Get option value
const value = ctx.getOption("name") orelse "default";
// Check if option was provided
if (ctx.hasOption("verbose")) {
// Do something
}
// Get positional argument
const arg = ctx.getArgument(0) orelse return error.MissingArgument;
// Get argument count
const count = ctx.getArgumentCount();
}
var text = prompt.TextPrompt.init(allocator, "Enter value:");
defer text.deinit();
_ = text.withPlaceholder("placeholder text");
_ = text.withDefault("default value");
_ = text.withValidation(myValidator);
const value = try text.prompt();
defer allocator.free(value);
Custom validator:
fn myValidator(value: []const u8) ?[]const u8 {
if (value.len < 3) {
return "Value must be at least 3 characters";
}
return null; // Valid
}
var confirm = prompt.ConfirmPrompt.init(allocator, "Continue?");
defer confirm.deinit();
_ = confirm.withDefault(true);
const result = try confirm.prompt(); // Returns bool
const choices = [_]prompt.SelectPrompt.Choice{
.{ .label = "TypeScript", .value = "ts", .description = "JavaScript with types" },
.{ .label = "Zig", .value = "zig", .description = "Systems programming" },
};
var select = prompt.SelectPrompt.init(allocator, "Choose a language:", &choices);
defer select.deinit();
const selected = try select.prompt();
defer allocator.free(selected);
const choices = [_]prompt.SelectPrompt.Choice{
.{ .label = "Option 1", .value = "opt1" },
.{ .label = "Option 2", .value = "opt2" },
};
var multi = try prompt.MultiSelectPrompt.init(allocator, "Select options:", &choices);
defer multi.deinit();
const selected = try multi.prompt(); // Returns [][]const u8
defer {
for (selected) |item| allocator.free(item);
allocator.free(selected);
}
var password = prompt.PasswordPrompt.init(allocator, "Enter password:");
defer password.deinit();
_ = password.withMaskChar('*');
_ = password.withValidation(validatePassword);
const pwd = try password.prompt();
defer allocator.free(pwd);
var spinner = prompt.SpinnerPrompt.init(allocator, "Loading data...");
try spinner.start();
// Do some work
std.time.sleep(2 * std.time.ns_per_s);
try spinner.stop("Data loaded successfully!");
// Intro/Outro for CLI flows
try prompt.intro(allocator, "My CLI Application");
// ... your application logic ...
try prompt.outro(allocator, "All done! Thanks for using our CLI.");
// Notes and logs
try prompt.note(allocator, "Important", "This is additional information");
try prompt.log(allocator, .info, "Starting process...");
try prompt.log(allocator, .success, "Process completed!");
try prompt.log(allocator, .warning, "This is a warning");
try prompt.log(allocator, .error_level, "An error occurred");
// Cancel message
try prompt.cancel(allocator, "Operation was canceled");
// Simple box
try prompt.box(allocator, "Title", "This is the content");
// Custom box with styling
var box = prompt.Box.init(allocator);
box = box.withStyle(.rounded); // .single, .double, .rounded, .ascii
box = box.withPadding(2);
try box.render("My Box",
\\Line 1 of content
\\Line 2 of content
\\Line 3 of content
);
var num_prompt = prompt.NumberPrompt.init(allocator, "Enter port:", .integer);
defer num_prompt.deinit();
_ = num_prompt.withRange(1, 65535); // Set min/max
_ = num_prompt.withDefault(8080);
const port = try num_prompt.prompt(); // Returns f64
const port_int = @as(u16, @intFromFloat(port));
Number types:
.integer - Integer values.float - Floating-point valuesvar path_prompt = prompt.PathPrompt.init(allocator, "Select file:", .file);
defer path_prompt.deinit();
_ = path_prompt.withMustExist(true); // Must exist
_ = path_prompt.withDefault("./config.toml");
const path = try path_prompt.prompt();
defer allocator.free(path);
// Press Tab to autocomplete based on filesystem
Path types:
.file - File selection.directory - Directory selection.any - File or directoryconst prompts = [_]prompt.GroupPrompt.PromptDef{
.{ .text = .{ .key = "name", .message = "Your name?" } },
.{ .number = .{ .key = "age", .message = "Your age?", .number_type = .integer } },
.{ .confirm = .{ .key = "agree", .message = "Do you agree?" } },
.{ .select = .{
.key = "lang",
.message = "Choose language:",
.choices = &[_]prompt.SelectPrompt.Choice{
.{ .label = "Zig", .value = "zig" },
.{ .label = "TypeScript", .value = "ts" },
},
}},
};
var group = prompt.GroupPrompt.init(allocator, &prompts);
defer group.deinit();
try group.run();
// Access results by key
const name = group.getText("name");
const age = group.getNumber("age");
const agreed = group.getBool("agree");
const lang = group.getText("lang");
var progress = prompt.ProgressBar.init(allocator, 100, "Processing files");
defer progress.deinit();
try progress.start();
for (0..100) |i| {
// Do some work
std.time.sleep(50 * std.time.ns_per_ms);
try progress.update(i + 1);
}
try progress.finish();
Progress bar styles:
.bar - Classic progress bar (█████░░░░░).blocks - Block characters (▓▓▓▓▓░░░░░).dots - Dots (⣿⣿⣿⣿⣿⡀⡀⡀⡀⡀).ascii - ASCII fallback ([====------])const columns = [_]prompt.Table.Column{
.{ .header = "Name", .alignment = .left },
.{ .header = "Age", .alignment = .right },
.{ .header = "Status", .alignment = .center },
};
var table = prompt.Table.init(allocator, &columns);
defer table.deinit();
table = table.withStyle(.rounded); // .simple, .rounded, .double, .minimal
try table.addRow(&[_][]const u8{ "Alice", "30", "Active" });
try table.addRow(&[_][]const u8{ "Bob", "25", "Inactive" });
try table.addRow(&[_][]const u8{ "Charlie", "35", "Active" });
try table.render();
// Create styled text with chainable API
const styled = try prompt.style(allocator, "Error occurred")
.red()
.bold()
.underline()
.render();
defer allocator.free(styled);
try prompt.Terminal.init().write(styled);
// Available colors: black, red, green, yellow, blue, magenta, cyan, white
// Available styles: bold(), dim(), italic(), underline()
// Available backgrounds: bgRed(), bgGreen(), bgBlue(), etc.
zig-cli supports loading configuration from TOML, JSONC (JSON with Comments), and JSON5 files.
// Load from file (auto-detects format)
var config = try cli.config.load(allocator, "config.toml");
defer config.deinit();
// Or load from string
var config2 = cli.config.Config.init(allocator);
defer config2.deinit();
try config2.loadFromString(content, .toml); // or .jsonc, .json5
// Auto-discover config file
var config3 = try cli.config.discover(allocator, "myapp");
defer config3.deinit();
// Searches for: myapp.toml, myapp.json5, myapp.jsonc, myapp.json
// In: ., ./.config, ~/.config/myapp
// Get typed values
if (config.getString("name")) |name| {
std.debug.print("Name: {s}\n", .{name});
}
if (config.getInt("port")) |port| {
std.debug.print("Port: {d}\n", .{port});
}
if (config.getBool("debug")) |debug| {
std.debug.print("Debug: {}\n", .{debug});
}
if (config.getFloat("timeout")) |timeout| {
std.debug.print("Timeout: {d}s\n", .{timeout});
}
// Get raw value for complex types
if (config.get("database")) |db_value| {
// Handle nested tables, arrays, etc.
}
TOML:
# config.toml
name = "myapp"
port = 8080
[database]
host = "localhost"
JSONC (JSON with Comments):
{
// Comments are allowed
"name": "myapp",
"port": 8080,
"database": {
"host": "localhost"
}, // trailing commas allowed
}
JSON5:
{
// Unquoted keys
name: 'myapp', // single quotes
port: 8080,
permissions: 0x755, // hex numbers
ratio: .5, // leading decimal
maxValue: Infinity, // special values
}
const ansi = @import("zig-cli").prompt.Ansi;
const colored = try ansi.colorize(allocator, "text", .green);
defer allocator.free(colored);
// Convenience functions
const bold = try ansi.bold(allocator, "text");
const red = try ansi.red(allocator, "error");
const green = try ansi.green(allocator, "success");
const symbols = ansi.Symbols.forTerminal(supports_unicode);
std.debug.print("{s} Success!\n", .{symbols.checkmark});
std.debug.print("{s} Error!\n", .{symbols.cross});
std.debug.print("{s} Loading...\n", .{symbols.spinner[0]});
Check out the examples/ directory for complete examples:
basic.zig - Basic CLI with options and subcommandsprompts.zig - All prompt types with validationadvanced.zig - Complex CLI with multiple commands and argumentsshowcase.zig - Comprehensive feature demonstration including all new promptsconfig.zig - Configuration file examples (TOML, JSONC, JSON5)Example config files are in examples/configs/:
example.toml - TOML format exampleexample.jsonc - JSONC format exampleexample.json5 - JSON5 format exampleRun examples with your own Zig project by importing zig-cli.
CLI
├── Command (root)
│ ├── Options (parsed from --flags)
│ ├── Arguments (positional)
│ └── Subcommands (nested)
└── Parser (validation pipeline)
PromptCore (state machine)
├── Terminal I/O
│ ├── Raw mode handling
│ ├── Keyboard input
│ └── ANSI output
├── State: initial → active ↔ error → submit/cancel
└── Events: value, cursor, key, submit, cancel
zig-cli is inspired by the TypeScript library clapp, bringing similar developer experience to Zig:
| Feature | clapp | zig-cli |
|---|---|---|
| Builder Pattern | ✅ | ✅ |
| Subcommands | ✅ | ✅ |
| Command Aliases | ✅ | ✅ |
| Interactive Prompts | ✅ | ✅ |
| State Machine | ✅ | ✅ |
| Type Validation | ✅ | ✅ |
| ANSI Colors | ✅ | ✅ |
| Style Chaining | ✅ | ✅ |
| Spinner/Loading | ✅ | ✅ |
| Progress Bars | ✅ | ✅ |
| Box Rendering | ✅ | ✅ |
| Table Rendering | ✅ | ✅ |
| Message Prompts | ✅ | ✅ |
| Number Prompts | ✅ | ✅ |
| Path Prompts | ✅ | ✅ |
| Group Prompts | ✅ | ✅ |
| Terminal Detection | ✅ | ✅ |
| Dimension Detection | ✅ | ✅ |
| Config Files (TOML/JSONC/JSON5) | ✅ | ✅ |
| Middleware System | ✅ | ✅ |
| Language | TypeScript | Zig |
| Binary Size | ~50MB (with Node.js) | ~500KB |
| Startup Time | ~50-100ms | <1ms |
zig build test
MIT
Contributions are welcome! Please feel free to submit a Pull Request.
Spinner/loading indicators
Box/panel rendering
Message prompts (intro, outro, note, log, cancel)
Terminal dimension detection
Command aliases
Config file support (TOML, JSONC, JSON5)
Auto-discovery of config files
Progress bars with multiple styles
Table rendering with column alignment
Style chaining (.red().bold().underline())
Group prompts with result access
Number prompt with range validation
Path prompt with autocomplete
Middleware system for commands
Tree rendering for hierarchical data
Date/time prompts
Shell completion generation (bash, zsh, fish)
Better Windows terminal support
Task prompts with status indicators
Streaming output prompts
Vim keybindings for prompts
Multi-column layout support