derive-wizard

Crates.ioderive-wizard
lib.rsderive-wizard
version0.5.1-use-the-elicitor-crate-instead
created_at2025-12-27 11:05:11.862357+00
updated_at2026-01-17 11:51:40.772386+00
descriptionDerives interactive wizard-like user input for Rust types. Backend-agnostic (supports at least requestty and egui).
homepage
repositoryhttps://github.com/barafael/derive-wizard
max_upload_size
id2007046
size476,564
Rafael Bachmann (barafael)

documentation

README

derive-wizard

DEPRECATED: This crate has been renamed to elicitor. Please use elicitor for all new projects. This crate will no longer receive updates. Elicitor is much better.

A Rust procedural macro that automatically generates interactive CLI wizards from struct definitions using requestty.

Showcase

use derive_wizard::Wizard;

#[derive(Debug, Wizard)]
struct ShowCase {
    // String types - defaults to 'input'
    #[prompt("Enter your name:")]
    name: String,

    // Override with password question type
    #[prompt("Enter your password:")]
    #[mask]
    password: String,

    // Long text with multiline editor
    #[prompt("Enter a bio:")]
    #[multiline]
    bio: String,

    // Bool type - defaults to 'confirm'
    #[prompt("Do you agree to the terms?")]
    agree: bool,

    // Integer types - defaults to 'int'
    #[prompt("Enter your age (i32):")]
    age: i32,

    // Float types - defaults to 'float'
    #[prompt("Enter your height in meters (f64):")]
    height: f64,

    #[prompt("Enter a decimal number (f32):")]
    decimal: f32,
    
    #[prompt("Enter your gender")]
    gender: Gender,
}

#[derive(Debug, Wizard)]
enum Gender {
    Male,
    Female,
    Other(
        #[prompt("Please specify:")]
        String
    ),
}

Password Fields with #[mask]

For password inputs, use the convenient #[mask] attribute to hide user input:

use derive_wizard::Wizard;

#[derive(Debug, Wizard)]
struct LoginForm {
    #[prompt("Enter your username:")]
    username: String,

    #[prompt("Enter your password:")]
    #[mask]
    password: String,  // Input will be hidden
}

Long Text with #[multiline]

For longer text input, use the #[multiline] attribute to open the user's preferred text editor:

use derive_wizard::Wizard;

#[derive(Debug, Wizard)]
struct Article {
    #[prompt("Enter the title:")]
    title: String,

    #[prompt("Write the article content:")]
    #[multiline]
    content: String,  // Opens text editor (vim, nano, etc.)
}

Attributes

  • #[prompt("message")] - Required. The message to display to the user
  • #[mask] - Optional. For String fields: enables password input (hidden text)
  • #[multiline] - Optional. For String fields: opens text editor for longer input
  • #[validate("function_name")] - Optional. Validates input with a custom function

Note: #[mask] and #[multiline] are mutually exclusive and cannot be used on the same field.

Using the Builder API

The builder API provides a fluent interface for configuring and executing wizards:

use derive_wizard::Wizard;

#[derive(Debug, Clone, Wizard)]
struct Config {
    #[prompt("Enter the server address:")]
    server: String,

    #[prompt("Enter the port:")]
    port: u16,

    #[prompt("Enable SSL?")]
    use_ssl: bool,
}

// Simple usage with default backend (requestty)
let config = Config::wizard_builder().build().unwrap();
println!("Config: {config:#?}");

// Edit configuration with suggestions pre-filled
let updated_config = Config::wizard_builder()
    .with_suggestions(config)
    .build()
    .unwrap();
println!("Updated config: {updated_config:#?}");

Additional examples:

use derive_wizard::Wizard;

# #[derive(Debug, Clone, Wizard)]
# struct Config {
#     #[prompt("Enter the server address:")]
#     server: String,
#     #[prompt("Enter the port:")]
#     port: u16,
#     #[prompt("Enable SSL?")]
#     use_ssl: bool,
# }
// With custom backend (e.g., requestty)
let backend = derive_wizard::RequesttyBackend::new();
let config = Config::wizard_builder()
    .with_backend(backend)
    .build()
    .unwrap();
println!("Config: {config:#?}");

// Combine suggestions with custom backend
let backend = derive_wizard::RequesttyBackend::new();
let updated_config = Config::wizard_builder()
    .with_suggestions(config)
    .with_backend(backend)
    .build()
    .unwrap();
println!("Updated config: {updated_config:#?}");

When with_suggestions() is used:

  • For String fields: the current value is shown as a hint/placeholder
  • For numeric fields (integers and floats): the current value is shown as suggested
  • For bool fields: the current value is pre-selected
  • For password (#[mask]) and multiline (#[multiline]) fields: suggestions are shown as hints (backend-dependent)

Suggesting Individual Fields

Instead of providing a complete struct with with_suggestions(), you can suggest values for specific fields using suggest_field():

use derive_wizard::Wizard;

# #[derive(Debug, Clone, Wizard)]
# struct Config {
#     #[prompt("Enter the server address:")]
#     server: String,
#     #[prompt("Enter the port:")]
#     port: u16,
#     #[prompt("Enable SSL?")]
#     use_ssl: bool,
# }
// Suggest specific fields, ask about all of them
let config = Config::wizard_builder()
    .suggest_field("server", "localhost".to_string())
    .suggest_field("port", 8080)
    .suggest_field("use_ssl", false)
    .build();  // All questions asked with pre-filled defaults

This is useful when you want to provide defaults for specific fields without needing to construct an entire struct.

Using Assumptions

Assumptions are different from suggestions - they completely skip the questions and use the provided values directly. Use assume_field() to set specific fields while still asking about others:

use derive_wizard::Wizard;

# #[derive(Debug, Clone, Wizard)]
# struct Config {
#     #[prompt("Enter the server address:")]
#     server: String,
#     #[prompt("Enter the port:")]
#     port: u16,
#     #[prompt("Enable SSL?")]
#     use_ssl: bool,
# }
// Assume specific fields, ask about the rest
let config = Config::wizard_builder()
    .assume_field("use_ssl", true)      // Always use SSL in production
    .assume_field("port", 443)           // Standard HTTPS port
    .build();  // Will only ask about 'server'

Key differences:

  • Suggestions (with_suggestions() or suggest_field()): Questions are asked, but with pre-filled default values
  • Assumptions (assume_field()): Questions are skipped entirely, values are used as-is

You can also combine both approaches:

use derive_wizard::Wizard;

# #[derive(Debug, Clone, Wizard)]
# struct Config {
#     #[prompt("Enter the server address:")]
#     server: String,
#     #[prompt("Enter the port:")]
#     port: u16,
#     #[prompt("Enable SSL?")]
#     use_ssl: bool,
# }
let config = Config::wizard_builder()
    .suggest_field("server", "localhost".to_string())  // Suggest (will ask)
    .assume_field("use_ssl", true)                      // Assume (will skip)
    .assume_field("port", 443)                          // Assume (will skip)
    .build();  // Only asks about 'server' with "localhost" as default

Assumptions are useful for:

  • Enforcing security policies (e.g., always enable SSL in production)
  • Providing sensible defaults that users shouldn't change
  • Batch processing with some fixed and some variable fields

Working with Nested Fields

When your struct contains other Wizard-derived types, the fields are automatically namespaced with dot notation to avoid conflicts:

use derive_wizard::Wizard;

#[derive(Debug, Clone, Wizard)]
struct Address {
    #[prompt("Street:")]
    street: String,
    
    #[prompt("City:")]
    city: String,
}

#[derive(Debug, Clone, Wizard)]
struct UserProfile {
    #[prompt("Name:")]
    name: String,
    
    #[prompt("Home address:")]
    address: Address,  // Nested Wizard type
}

// The nested Address fields are automatically prefixed:
// - "address.street"
// - "address.city"

Namespace Prefixing: Each nested field is prefixed with its parent field name and a dot. This allows you to:

  • Have duplicate field names in different nested structures
  • Target specific nested fields with suggestions and assumptions
  • Maintain a flat question list while preserving logical structure

Using the field! Macro for Nested Fields

To reference nested fields in suggest_field() or assume_field(), use the field! macro with dot notation:

use derive_wizard::{Wizard, field};

# #[derive(Debug, Clone, Wizard)]
# struct Address {
#     #[prompt("Street:")]
#     street: String,
#     #[prompt("City:")]
#     city: String,
# }
# 
# #[derive(Debug, Clone, Wizard)]
# struct UserProfile {
#     #[prompt("Name:")]
#     name: String,
#     #[prompt("Address:")]
#     address: Address,
# }
let profile = UserProfile::wizard_builder()
    .suggest_field(field!(name), "John Doe".to_string())
    .suggest_field(
        field!(UserProfile::address::street),
        "123 Main St".to_string()
    )
    .assume_field(
        field!(UserProfile::address::city),
        "Springfield".to_string()
    )
    .build();

The field! macro supports:

  • Simple fields: field!(name)"name"
  • One level nesting: field!(Type::field)"field"
  • Two level nesting: field!(Type::nested::field)"nested.field"

Handling Duplicate Field Names

Namespace prefixing automatically handles duplicate field names across different nested structures:

use derive_wizard::{Wizard, field};

#[derive(Debug, Clone, Wizard)]
struct Department {
    #[prompt("Department name:")]
    name: String,
    
    #[prompt("Budget:")]
    budget: i32,
}

#[derive(Debug, Clone, Wizard)]
struct Organization {
    #[prompt("Organization name:")]
    name: String,  // Same field name as Department
    
    #[prompt("Primary department:")]
    primary: Department,
    #[prompt("Secondary department:")]
    secondary: Department,
}

// Each 'name' field gets a unique path:
// - "name" (Organization.name)
// - "primary.name" (primary Department.name)
// - "secondary.name" (secondary Department.name)

let org = Organization::wizard_builder()
    .suggest_field(field!(name), "Acme Corp".to_string())
    .assume_field(
        field!(Organization::primary::name),
        "Engineering".to_string()
    )
    .assume_field(
        field!(Organization::secondary::name),
        "Sales".to_string()
    )
    .build();

This namespace approach ensures that:

  • No field names collide, even with identical names in different nested structures
  • You can precisely target any field using the field! macro
  • The interview remains a flat list of questions (no complex nesting UI)

Supported Question Types

The #[derive(Wizard)] macro supports all 11 requestty question types:

Rust Type Default Question Type Override Options Returns
String input #[mask] for password, #[multiline] for text editor String
bool confirm - bool
i8, i16, i32, i64, isize int - i64 (cast to type)
u8, u16, u32, u64, usize int - i64 (cast to type)
f32, f64 float - f64 (cast to type)
ListItem select - ListItem
ExpandItem expand - ExpandItem
Vec<ListItem> multi_select - Vec<ListItem>

Question Type Details

  1. input - Basic text input prompt (default for String)
  2. password - Hidden text input (use #[mask] on String fields)
  3. editor - Opens text editor for longer input (use #[multiline] on String fields)
  4. confirm - Yes/No confirmation prompt (default for bool)
  5. int - Integer input (default for integer types)
  6. float - Floating point input (default for float types)
  7. select - Single selection from a list (default for ListItem)
  8. expand - Single selection with keyboard shortcuts (default for ExpandItem)
  9. multi_select - Multiple selection from a list (default for Vec<ListItem>)

Note: The following question types are available in requestty but not currently exposed through attributes:

  • raw_select - Single selection with index-based input
  • order_select - Reorder items in a list

License

MIT OR Apache-2.0

Commit count: 141

cargo fmt