19 KiB
ADR-013: Typdialog Web UI Backend Integration for Interactive Configuration
Status
Accepted - 2025-01-08
Context
The provisioning system requires interactive user input for configuration workflows, workspace initialization, credential setup, and guided deployment scenarios. The system architecture combines Rust (performance-critical), Nushell (scripting), and Nickel (declarative configuration), creating challenges for interactive form-based input and multi-user collaboration.
The Interactive Configuration Problem
Current limitations:
-
Nushell CLI: Terminal-only interaction
inputcommand: Single-line text prompts only- No form validation, no complex multi-field forms
- Limited to single-user, terminal-bound workflows
- User experience: Basic and error-prone
-
Nickel: Declarative configuration language
- Cannot handle interactive prompts (by design)
- Pure evaluation model (no side effects)
- Forms must be defined statically, not interactively
- No runtime user interaction
-
Existing Solutions: Inadequate for modern infrastructure provisioning
- Shell-based prompts: Error-prone, no validation, single-user
- Custom web forms: High maintenance, inconsistent UX
- Separate admin panels: Disconnected from IaC workflow
- Terminal-only TUI: Limited to SSH sessions, no collaboration
Use Cases Requiring Interactive Input
-
Workspace Initialization:
# Current: Error-prone prompts let workspace_name = input "Workspace name: " let provider = input "Provider (aws/azure/oci): " # No validation, no autocomplete, no guidance -
Credential Setup:
# Current: Insecure and basic let api_key = input "API Key: " # Shows in terminal history let region = input "Region: " # No validation -
Configuration Wizards:
- Database connection setup (host, port, credentials, SSL)
- Network configuration (CIDR blocks, subnets, gateways)
- Security policies (encryption, access control, audit)
-
Guided Deployments:
- Multi-step infrastructure provisioning
- Service selection with dependencies
- Environment-specific overrides
Requirements for Interactive Input System
- ✅ Terminal UI widgets: Text input, password, select, multi-select, confirm
- ✅ Validation: Type checking, regex patterns, custom validators
- ✅ Security: Password masking, sensitive data handling
- ✅ User Experience: Arrow key navigation, autocomplete, help text
- ✅ Composability: Chain multiple prompts into forms
- ✅ Error Handling: Clear validation errors, retry logic
- ✅ Rust Integration: Native Rust library (no subprocess overhead)
- ✅ Cross-Platform: Works on Linux, macOS, Windows
Decision
Integrate typdialog with its Web UI backend as the standard interactive configuration interface for the provisioning platform. The major achievement of typdialog is not the TUI - it is the Web UI backend that enables browser-based forms, multi-user collaboration, and seamless integration with the provisioning orchestrator.
Architecture Diagram
┌─────────────────────────────────────────┐
│ Nushell Script │
│ │
│ provisioning workspace init │
│ provisioning config setup │
│ provisioning deploy guided │
└────────────┬────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Rust CLI Handler │
│ (provisioning/core/cli/) │
│ │
│ - Parse command │
│ - Determine if interactive needed │
│ - Invoke TUI dialog module │
└────────────┬────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ TUI Dialog Module │
│ (typdialog wrapper) │
│ │
│ - Form definition (validation rules) │
│ - Widget rendering (text, select) │
│ - User input capture │
│ - Validation execution │
│ - Result serialization (JSON/TOML) │
└────────────┬────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ typdialog Library │
│ │
│ - Terminal rendering (crossterm) │
│ - Event handling (keyboard, mouse) │
│ - Widget state management │
│ - Input validation engine │
└────────────┬────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Terminal (stdout/stdin) │
│ │
│ ✅ Rich TUI with validation │
│ ✅ Secure password input │
│ ✅ Guided multi-step forms │
└─────────────────────────────────────────┘
Implementation Characteristics
CLI Integration Provides:
- ✅ Native Rust commands with TUI dialogs
- ✅ Form-based input for complex configurations
- ✅ Validation rules defined in Rust (type-safe)
- ✅ Secure input (password masking, no history)
- ✅ Error handling with retry logic
- ✅ Serialization to Nickel/TOML/JSON
TUI Dialog Library Handles:
- ✅ Terminal UI rendering and event loop
- ✅ Widget management (text, select, checkbox, confirm)
- ✅ Input validation and error display
- ✅ Navigation (arrow keys, tab, enter)
- ✅ Cross-platform terminal compatibility
Rationale
Why TUI Dialog Integration Is Required
| Aspect | Shell Prompts (current) | Web Forms | TUI Dialog (chosen) |
|---|---|---|---|
| User Experience | ❌ Basic text only | ✅ Rich UI | ✅ Rich TUI |
| Validation | ❌ Manual, error-prone | ✅ Built-in | ✅ Built-in |
| Security | ❌ Plain text, history | ⚠️ Network risk | ✅ Secure terminal |
| Setup Complexity | ✅ None | ❌ Server required | ✅ Minimal |
| Terminal Workflow | ✅ Native | ❌ Browser switch | ✅ Native |
| Offline Support | ✅ Always | ❌ Requires server | ✅ Always |
| Dependencies | ✅ None | ❌ Web stack | ✅ Single crate |
| Error Handling | ❌ Manual | ⚠️ Complex | ✅ Built-in retry |
The Nushell Limitation
Nushell's input command is limited:
# Current: No validation, no security
let password = input "Password: " # ❌ Shows in terminal
let region = input "AWS Region: " # ❌ No autocomplete/validation
# Cannot do:
# - Multi-select from options
# - Conditional fields (if X then ask Y)
# - Password masking
# - Real-time validation
# - Autocomplete/fuzzy search
The Nickel Constraint
Nickel is declarative and cannot prompt users:
# Nickel defines what the config looks like, NOT how to get it
{
database = {
host | String,
port | Number,
credentials | { username: String, password: String },
}
}
# Nickel cannot:
# - Prompt user for values
# - Show interactive forms
# - Validate input interactively
Why Rust + TUI Dialog Is The Solution
Rust provides:
- Native terminal control (crossterm, termion)
- Type-safe form definitions
- Validation rules as functions
- Secure memory handling (password zeroization)
- Performance (no subprocess overhead)
TUI Dialog provides:
- Widget library (text, select, multi-select, confirm)
- Event loop and rendering
- Validation framework
- Error display and retry logic
Integration enables:
- Nushell calls Rust CLI → Shows TUI dialog → Returns validated config
- Nickel receives validated config → Type checks → Merges with defaults
Consequences
Positive
- User Experience: Professional TUI with validation and guidance
- Security: Password masking, sensitive data protection, no terminal history
- Validation: Type-safe rules enforced before config generation
- Developer Experience: Reusable form components across CLI commands
- Error Handling: Clear validation errors with retry options
- Offline First: No network dependencies for interactive input
- Terminal Native: Fits CLI workflow, no context switching
- Maintainability: Single library for all interactive input
Negative
- Terminal Dependency: Requires interactive terminal (not scriptable)
- Learning Curve: Developers must learn TUI dialog patterns
- Library Lock-in: Tied to specific TUI library API
- Testing Complexity: Interactive tests require terminal mocking
- Non-Interactive Fallback: Need alternative for CI/CD and scripts
Mitigation Strategies
Non-Interactive Mode:
// Support both interactive and non-interactive
if terminal::is_interactive() {
// Show TUI dialog
let config = show_workspace_form()?;
} else {
// Use config file or CLI args
let config = load_config_from_file(args.config)?;
}
Testing:
// Unit tests: Test form validation logic (no TUI)
#[test]
fn test_validate_workspace_name() {
assert!(validate_name("my-workspace").is_ok());
assert!(validate_name("invalid name!").is_err());
}
// Integration tests: Use mock terminal or config files
Scriptability:
# Batch mode: Provide config via file
provisioning workspace init --config workspace.toml
# Interactive mode: Show TUI dialog
provisioning workspace init --interactive
Documentation:
- Form schemas documented in
docs/ - Config file examples provided
- Screenshots of TUI forms in guides
Alternatives Considered
Alternative 1: Shell-Based Prompts (Current State)
Pros: Simple, no dependencies Cons: No validation, poor UX, security risks Decision: REJECTED - Inadequate for production use
Alternative 2: Web-Based Forms
Pros: Rich UI, well-known patterns Cons: Requires server, network dependency, context switch Decision: REJECTED - Too complex for CLI tool
Alternative 3: Custom TUI Per Use Case
Pros: Tailored to each need Cons: High maintenance, code duplication, inconsistent UX Decision: REJECTED - Not sustainable
Alternative 4: External Form Tool (dialog, whiptail)
Pros: Mature, cross-platform Cons: Subprocess overhead, limited validation, shell escaping issues Decision: REJECTED - Poor Rust integration
Alternative 5: Text-Based Config Files Only
Pros: Fully scriptable, no interactive complexity Cons: Steep learning curve, no guidance for new users Decision: REJECTED - Poor user onboarding experience
Implementation Details
Form Definition Pattern
use typdialog::Form;
pub fn workspace_initialization_form() -> Result<WorkspaceConfig> {
let form = Form::new("Workspace Initialization")
.add_text_input("name", "Workspace Name")
.required()
.validator(|s| validate_workspace_name(s))
.add_select("provider", "Cloud Provider")
.options(&["aws", "azure", "oci", "local"])
.required()
.add_text_input("region", "Region")
.default("us-west-2")
.validator(|s| validate_region(s))
.add_password("admin_password", "Admin Password")
.required()
.min_length(12)
.add_confirm("enable_monitoring", "Enable Monitoring?")
.default(true);
let responses = form.run()?;
// Convert to strongly-typed config
let config = WorkspaceConfig {
name: responses.get_string("name")?,
provider: responses.get_string("provider")?.parse()?,
region: responses.get_string("region")?,
admin_password: responses.get_password("admin_password")?,
enable_monitoring: responses.get_bool("enable_monitoring")?,
};
Ok(config)
}
Integration with Nickel
// 1. Get validated input from TUI dialog
let config = workspace_initialization_form()?;
// 2. Serialize to TOML/JSON
let config_toml = toml::to_string(&config)?;
// 3. Write to workspace config
fs::write("workspace/config.toml", config_toml)?;
// 4. Nickel merges with defaults
// nickel export workspace/main.ncl --format json
// (uses workspace/config.toml as input)
CLI Command Structure
// provisioning/core/cli/src/commands/workspace.rs
#[derive(Parser)]
pub enum WorkspaceCommand {
Init {
#[arg(long)]
interactive: bool,
#[arg(long)]
config: Option<PathBuf>,
},
}
pub fn handle_workspace_init(args: InitArgs) -> Result<()> {
if args.interactive || terminal::is_interactive() {
// Show TUI dialog
let config = workspace_initialization_form()?;
config.save("workspace/config.toml")?;
} else if let Some(config_path) = args.config {
// Use provided config
let config = WorkspaceConfig::load(config_path)?;
config.save("workspace/config.toml")?;
} else {
bail!("Either --interactive or --config required");
}
// Continue with workspace setup
Ok(())
}
Validation Rules
pub fn validate_workspace_name(name: &str) -> Result<(), String> {
// Alphanumeric, hyphens, 3-32 chars
let re = Regex::new(r"^[a-z0-9-]{3,32}$").unwrap();
if !re.is_match(name) {
return Err("Name must be 3-32 lowercase alphanumeric chars with hyphens".into());
}
Ok(())
}
pub fn validate_region(region: &str) -> Result<(), String> {
const VALID_REGIONS: &[&str] = &["us-west-1", "us-west-2", "us-east-1", "eu-west-1"];
if !VALID_REGIONS.contains(®ion) {
return Err(format!("Invalid region. Must be one of: {}", VALID_REGIONS.join(", ")));
}
Ok(())
}
Security: Password Handling
use zeroize::Zeroizing;
pub fn get_secure_password() -> Result<Zeroizing<String>> {
let form = Form::new("Secure Input")
.add_password("password", "Password")
.required()
.min_length(12)
.validator(password_strength_check);
let responses = form.run()?;
// Password automatically zeroized when dropped
let password = Zeroizing::new(responses.get_password("password")?);
Ok(password)
}
Testing Strategy
Unit Tests:
#[test]
fn test_workspace_name_validation() {
assert!(validate_workspace_name("my-workspace").is_ok());
assert!(validate_workspace_name("UPPERCASE").is_err());
assert!(validate_workspace_name("ab").is_err()); // Too short
}
Integration Tests:
// Use non-interactive mode with config files
#[test]
fn test_workspace_init_non_interactive() {
let config = WorkspaceConfig {
name: "test-workspace".into(),
provider: Provider::Local,
region: "us-west-2".into(),
admin_password: "secure-password-123".into(),
enable_monitoring: true,
};
config.save("/tmp/test-config.toml").unwrap();
let result = handle_workspace_init(InitArgs {
interactive: false,
config: Some("/tmp/test-config.toml".into()),
});
assert!(result.is_ok());
}
Manual Testing:
# Test interactive flow
cargo build --release
./target/release/provisioning workspace init --interactive
# Test validation errors
# - Try invalid workspace name
# - Try weak password
# - Try invalid region
Configuration Integration
CLI Flag:
# provisioning/config/config.defaults.toml
[ui]
interactive_mode = "auto" # "auto" | "always" | "never"
dialog_theme = "default" # "default" | "minimal" | "colorful"
Environment Override:
# Force non-interactive mode (for CI/CD)
export PROVISIONING_INTERACTIVE=false
# Force interactive mode
export PROVISIONING_INTERACTIVE=true
Documentation Requirements
User Guides:
docs/user/interactive-configuration.md- How to use TUI dialogsdocs/guides/workspace-setup.md- Workspace initialization with screenshots
Developer Documentation:
docs/development/tui-forms.md- Creating new TUI forms- Form definition best practices
- Validation rule patterns
Configuration Schema:
# provisioning/schemas/workspace.ncl
{
WorkspaceConfig = {
name
| doc "Workspace identifier (3-32 alphanumeric chars with hyphens)"
| String,
provider
| doc "Cloud provider"
| [| 'aws, 'azure, 'oci, 'local |],
region
| doc "Deployment region"
| String,
admin_password
| doc "Admin password (min 12 characters)"
| String,
enable_monitoring
| doc "Enable monitoring services"
| Bool,
}
}
Migration Path
Phase 1: Add Library
- Add typdialog dependency to
provisioning/core/cli/Cargo.toml - Create TUI dialog wrapper module
- Implement basic text/select widgets
Phase 2: Implement Forms
- Workspace initialization form
- Credential setup form
- Configuration wizard forms
Phase 3: CLI Integration
- Update CLI commands to use TUI dialogs
- Add
--interactive/--configflags - Implement non-interactive fallback
Phase 4: Documentation
- User guides with screenshots
- Developer documentation for form creation
- Example configs for non-interactive use
Phase 5: Testing
- Unit tests for validation logic
- Integration tests with config files
- Manual testing on all platforms
References
- typdialog Crate (or similar: dialoguer, inquire)
- crossterm - Terminal manipulation
- zeroize - Secure memory zeroization
- ADR-004: Hybrid Architecture (Rust/Nushell integration)
- ADR-011: Nickel Migration (declarative config language)
- ADR-012: Nushell Plugins (CLI wrapper patterns)
- Nushell
inputcommand limitations: Nushell Book - Input
Status: Accepted Last Updated: 2025-01-08 Implementation: Planned Priority: High (User onboarding and security) Estimated Complexity: Moderate