TheContextcarries execution metadata — verbosity level, output format, color preference, and user-defined global options — to every command handler.
Injecting context
Add actx: Contextparameter to any command handler. The CLI dispatcher injects it automatically and excludes it from argparse and MCP schemas:
from milo import CLI, Context
cli = CLI(name="myapp")
@cli.command("build", description="Build the site")
def build(output: str = "_site", ctx: Context = None) -> str:
ctx.log("Starting build...", level=1) # only shown with --verbose
ctx.log(f"Output dir: {output}", level=2) # only shown with -vv
return f"Built to {output}"
Verbosity levels
The CLI maps flags to verbosity integers:
| Flag | ctx.verbosity |
Property |
|---|---|---|
-q / --quiet |
-1 | ctx.quiet == True |
| (default) | 0 | — |
-v / --verbose |
1 | ctx.verbose == True |
-vv |
2 | ctx.debug == True |
Usectx.log()to print messages at specific verbosity levels:
ctx.log("Always shown unless quiet", level=0)
ctx.log("Verbose detail", level=1)
ctx.log("Debug trace", level=2)
Messages go to stderr, keeping stdout clean for structured output.
Global options
Register CLI-wide options that are available on every command viactx.globals:
cli.global_option("environment", short="-e", default="local",
description="Target environment")
cli.global_option("dry_run", is_flag=True,
description="Simulate without making changes")
Access them in handlers:
@cli.command("deploy", description="Deploy the app")
def deploy(service: str, ctx: Context = None) -> dict:
env = ctx.globals.get("environment", "local")
dry = ctx.globals.get("dry_run", False)
if dry:
return {"action": "dry-run", "service": service, "env": env}
return {"action": "deployed", "service": service, "env": env}
myapp deploy --service api -e staging --dry-run
get_context()
For library code that doesn't have direct access to thectx parameter, use get_context():
from milo import get_context
def helper():
ctx = get_context()
ctx.log("Called from library code", level=1)
get_context() uses a ContextVar set by the CLI dispatcher. If no context has been set, it returns a default Context.
Running interactive apps from commands
Usectx.run_app() to launch an interactive Appfrom within a CLI command handler. The command blocks while the app runs and receives the final state when it exits:
from dataclasses import dataclass
from milo import CLI, Context, quit_on, with_cursor, with_confirm, SpecialKey
cli = CLI(name="picker")
@dataclass(frozen=True, slots=True)
class PickState:
items: tuple[str, ...] = ("alpha", "beta", "gamma")
cursor: int = 0
@with_confirm()
@with_cursor("items", wrap=True)
@quit_on(SpecialKey.ESCAPE)
def reducer(state, action):
if state is None:
return PickState()
return state
@cli.command("pick", description="Pick an item")
def pick(ctx: Context = None) -> str:
state = ctx.run_app(reducer, template="pick.kida", initial_state=PickState())
return state.items[state.cursor]
This bridges the CLI dispatch layer with the Elm Architecture event loop — the command defines what to run, the app handles how to interact.
Context fields
| Field | Type | Description |
|---|---|---|
verbosity |
int |
-1=quiet, 0=normal, 1=verbose, 2=debug |
format |
str |
Output format:"plain", "json", "table" |
color |
bool |
Whether color output is enabled |
globals |
dict |
Values from user-defined global options |
Tip
Combine with Configuration to let global options control config loading — for example, a--profileoption that selects a config profile.