Build an Install Wizard

Build a multi-screen install wizard with forms, validation, and side effects.

3 min read 555 words

This tutorial walks you through building a multi-screen install wizard — the kind of interactive CLI you'd use for setting up a tool, configuring a service, or onboarding a user.

What you'll build

flowchart LR W[Welcome] -->|Enter| C[Config Form] C -->|Submit| I[Install] I -->|saga completes| Done[Exit]

A three-screen wizard:

  1. Welcome — greet the user, explain what happens next
  2. Config — collect a project name, environment, and confirmation via form fields
  3. Install — run the install saga, show a progress bar, display results

Prerequisites

2/3 complete

Build the Wizard

  1. 1

    Define the screens

    Create three FlowScreens with templates and reducers

    Each screen is aFlowScreenwith its own template and reducer:

    from milo.flow import FlowScreen
    
    welcome = FlowScreen("welcome", "welcome.kida", welcome_reducer)
    config = FlowScreen("config", "config.kida", config_reducer)
    install = FlowScreen("install", "install.kida", install_reducer)
    
  2. 2

    Write the welcome reducer

    Advance on Enter

    The welcome screen advances to config when the user presses Enter:

    def welcome_reducer(state, action):
        if state is None:
            return {"ready": False}
        if action.type == "@@KEY" and action.payload.name == "ENTER":
            return {**state, "ready": True, "submitted": True}
        return state
    
  3. 3

    Write the config reducer

    Use form_reducer for structured input

    The config screen usesform_reducerto handle form fields:

    from milo import FieldSpec, FieldType
    from milo.form import form_reducer
    
    config_specs = [
        FieldSpec("project", "Project name"),
        FieldSpec("env", "Environment", field_type=FieldType.SELECT,
                  choices=("dev", "staging", "prod")),
        FieldSpec("confirm", "Proceed with install?",
                  field_type=FieldType.CONFIRM),
    ]
    
    def config_reducer(state, action):
        if state is None:
            return form_reducer({"specs": config_specs}, action)
        return form_reducer(state, action)
    
  4. 4

    Write the install reducer and saga

    Trigger side effects with ReducerResult

    The install screen triggers a saga on entry and tracks progress:

    from milo import ReducerResult, Call, Put, Delay, Action
    
    def install_reducer(state, action):
        if state is None:
            return ReducerResult(
                {"progress": 0, "status": "installing", "log": []},
                sagas=(install_saga,),
            )
        if action.type == "PROGRESS":
            return {**state, "progress": action.payload}
        if action.type == "INSTALL_DONE":
            return {**state, "progress": 100, "status": "done",
                    "log": action.payload}
        return state
    
    def install_saga():
        for i in range(1, 11):
            yield Delay(0.3)
            yield Put(Action("PROGRESS", payload=i * 10))
        result = yield Call(run_install, ())
        yield Put(Action("INSTALL_DONE", payload=result))
    
  5. 5

    Create the templates

    Kida templates for each screen

    {{ "Install Wizard" | bold }}
    {{ "=" * 40 | fg("dim") }}
    
    This wizard will set up your project.
    
    Press ENTER to continue.
    
    {% include "form.kida" %}
    

    Uses the built-in form template.

    {{ "Installing..." | bold }}
    
    {% include "progress.kida" %}
    
    {% for line in log %}
    {{ line }}
    {% end %}
    
    {% if status == "done" %}
    {{ "Done!" | fg("green") | bold }}
    {% end %}
    
  6. 6

    Wire it up

    Chain screens and run

    from milo import App
    
    flow = welcome >> config >> install
    app = App.from_flow(flow)
    app.run()
    

Next steps