# Architecture Technical positioning and internal architecture of TypeDialog. ## Positioning TypeDialog is not a terminal prompt library — it is a **form orchestration layer** that sits two abstraction levels above tools like Inquire, Ratatui, or Axum. The comparison point is relevant: Inquire (`inquire = "0.9"`) is one of TypeDialog's dependencies, consumed as the render engine for the CLI backend. A user of TypeDialog never calls `Text::new("Name?").prompt()` directly — that call happens inside `InquireBackend::execute_field_sync`, invisible to the form author. ### Abstraction Stack ```text ┌─────────────────────────────────────────────────────┐ │ typedialog │ │ Declarative forms (Nickel / TOML) + i18n + AI │ │ load_form → BackendFactory → dispatch by feature │ ├──────────────┬──────────────┬──────────────────────-┤ │ CLI backend │ TUI backend │ Web backend │ │ (Inquire) │ (Ratatui) │ (Axum) │ └──────────────┴──────────────┴───────────────────────┘ │ Nushell plugin (nu-protocol) ``` ### What TypeDialog Is Not | What the primitive does | TypeDialog | |---|---| | Library for use in your CLI app | Is the app — or the library others embed | | Terminal input tool | Multi-surface entry point (CLI, TUI, Web, AI) | | Comparable to Rhai or Nushell | Integrates Nushell via `nu-plugin`/`nu-protocol` as an output channel | ### Differentiators **Unified schema** — a single Nickel (`.ncl`) or TOML file describes the same form for CLI, TUI, and HTTP. No per-backend code. See [ADR-001](./adr/adr-001-nickel-form-definition.md). **Nushell as integration runtime** — outputs flow as structured data into Nu pipelines via the Nushell plugin protocol (`nu-plugin = "0.110.0"`), not as text. **AI backend** (`typedialog-ai` + `typedialog-agent`) — forms can be generated or processed by agents backed by SurrealDB (`kv-mem`), Tantivy full-text search, and HNSW vector search (`instant-distance`). **Native i18n** — Fluent bundles with system locale detection, no external tooling required. --- ## Form Loading ### `load_form` — Unified Entry Point All binaries and roundtrip paths call `form_parser::load_form(path)`. Extension determines the loader: ```rust pub fn load_form(path: impl AsRef) -> Result { match path.as_ref().extension().and_then(|e| e.to_str()) { Some("ncl") => load_from_ncl(path), _ => load_from_file(path), // TOML path, unchanged } } ``` `.ncl` forms are loaded via `nickel export --format json` as a subprocess. If the export fails for any reason — contract violation, syntax error, missing import — the process aborts before any interaction begins. TOML forms are deserialized directly via `serde`. Fragment lazy-loading (`includes:` in TOML groups) is replaced by Nickel native imports. `build_element_list` is now a pure in-memory operation with no I/O. The functions `expand_includes` and `load_fragment_form` remain available in `fragments.rs` as rescue code. ### `when_false` — Conditional Field Output Fields with a `when:` condition that evaluates to false are skipped by default. `when_false = "default"` instructs the executor to inject the field's `default` value into results even when the condition is false: ```toml [fields.tls_cert_path] when = "tls_enabled == true" when_false = "default" default = "/etc/ssl/cert.pem" ``` This ensures Nickel roundtrip writers always have a value for every schema field regardless of which branches the user navigated. --- ## BackendFactory ### The `FormBackend` Trait All backends implement a single async trait (`backends/mod.rs:27`): ```rust #[async_trait] pub trait FormBackend: Send + Sync { async fn initialize(&mut self) -> Result<()>; async fn render_display_item(&self, item: &DisplayItem, context: &RenderContext) -> Result<()>; async fn execute_field(&self, field: &FieldDefinition, context: &RenderContext) -> Result; // Default impl: falls back to field-by-field via execute_field // TUI and Web override this for complete-form rendering async fn execute_form_complete(...) -> Result>; async fn shutdown(&mut self) -> Result<()>; fn as_any(&self) -> &dyn std::any::Any; fn is_available() -> bool where Self: Sized; fn name(&self) -> &str; } ``` `execute_form_complete` has a default implementation that delegates to `execute_field` sequentially. Backends that need full-form rendering (TUI, Web) override it. ### Factory Dispatch `BackendFactory::create` returns `Box`. The selection is a compile-time `#[cfg(feature)]` gate combined with runtime match (`backends/mod.rs:109`): ```rust pub fn create(backend_type: BackendType) -> Result> { match backend_type { BackendType::Cli => { #[cfg(feature = "cli")] { Ok(Box::new(cli::InquireBackend::new())) } #[cfg(not(feature = "cli"))] { Err(/* clear message */) } } #[cfg(feature = "tui")] BackendType::Tui => Ok(Box::new(tui::RatatuiBackend::new())), #[cfg(feature = "web")] BackendType::Web { port } => Ok(Box::new(web::WebBackend::new(port))), } } ``` The backend code for a disabled feature is not compiled into the binary. The trait object provides runtime polymorphism; the feature gate provides compile-time dead-code elimination. ### Auto-Detection `BackendFactory::auto_detect` follows a fallback chain (`backends/mod.rs:132`): ```text TYPEDIALOG_BACKEND=tui → TUI (if feature active) TYPEDIALOG_BACKEND=web → Web (port from TYPEDIALOG_PORT, default 9000) default → Cli ``` ### RenderContext Passed to every render and field execution call: ```rust pub struct RenderContext { pub results: HashMap, // accumulated field values pub locale: Option, // locale override } ``` `results` carries the accumulated answers from previous fields. `InquireBackend::execute_field_sync` receives this as `previous_results` and uses it for `options_from` filtering in `Select` fields. --- ## Form Execution ### Three-Phase Execution `execute_with_backend_two_phase_with_defaults` (`form_parser/executor.rs`) is the primary execution path: **Phase 1 — Selector fields first** Fields that control `when:` conditionals are identified and executed before Phase 2. This ensures conditional branches are known before iterating the element list. ```rust let selector_field_names = identify_selector_fields(&form); // execute only selectors → populate results ``` **Phase 2 — Build element list (pure)** With Phase 1 results known, the element list is built from the fully-composed in-memory form. No I/O occurs here — fragments were resolved by `load_form` before execution began. ```rust let element_list = build_element_list(&form); ``` **Phase 3 — Execute remaining fields** Iterates the element list, evaluates `when:` conditions per-element, applies `when_false` defaults for skipped fields, and dispatches to the backend. Two sub-modes: - `DisplayMode::Complete` — passes all items and fields to `execute_form_complete` at once (used by TUI and Web for reactive rendering) - Field-by-field — sequential execution with condition evaluation per step (used by CLI) ### Full Execution Flow ```text typedialog binary │ ├── form_parser::load_form(path) → FormDefinition (.ncl via nickel export | .toml via serde) ├── BackendFactory::create(BackendType::Cli) → Box ├── execute_with_backend_two_phase_with_defaults(form, backend, i18n, base_dir, defaults) │ │ │ ├── Phase 1: selector fields → backend.execute_field() │ ├── Phase 2: build_element_list() — pure, no I/O │ └── Phase 3: iterate → when/when_false → render_display_item() / execute_field() │ └── HashMap → JSON / YAML / TOML output ``` ### CLI Field Execution The `InquireBackend` implements field execution as a synchronous function (`backends/cli.rs`): ```rust pub(crate) fn execute_field_sync( &self, field: &FieldDefinition, previous_results: &HashMap, ) -> Result ``` The `FormBackend::execute_field` trait impl wraps it: ```rust async fn execute_field(&self, field: &FieldDefinition, context: &RenderContext) -> Result { self.execute_field_sync(field, &context.results) } ``` `filter_options_from` lives in `backends/cli.rs` as `pub(crate)` and is called from within `execute_field_sync` for `Select` fields. It filters the declared options to only those present in a referenced prior field's value — enabling dependent selects. ### Legacy Sync Path `execute_with_base_dir`, `execute`, and `load_and_execute_from_file` are a pre-`BackendFactory` sync path gated on `#[cfg(feature = "cli")]`. They call `InquireBackend::execute_field_sync` directly and bypass the async executor. Available for backward compatibility; the canonical path for new code is `execute_with_backend_two_phase_with_defaults`. --- ## Known Technical Debt No unresolved items.