Skip to content

Overview

Via the run function you get access to the CLI, possibly enriched from the config file. Then, you receive all the data in the m.env object, along with dialog methods in a proper UI.

graph LR
    subgraph mininterface
        run --> GUI
        run --> TUI
        run --> WebUI
        run --> env
        CLI --> run
        id1[config file] --> CLI
    end
    program --> run

Basic usage

Use a common dataclass, an argparse ArgumentParser, a Pydantic BaseModel or an attrs model to store the configuration. Wrap it in the run function that returns an interface m. Access the configuration via m.env or use it to prompt the user with methods like m.confirm("Is that alright?").

There are a lot of supported types you can use, not only scalars and well-known objects (Path, datetime), but also functions, iterables (like list[Path]) and union types (like int | None). To do even more advanced things, attach the value to a powerful Tag or its subclasses. E.g. for validation only, use its Validation alias.

Finally, use Facet to access the interface from the back-end (m) or the front-end (Tag) side.

with run() as m: — persistent window

Wrap your code in a with statement to keep the window open across multiple dialogs:

with run(Env) as m:
    print(f"Your number is {m.env.my_number}")
    if m.confirm("Is that alright?"):
        m.alert("Great!")

Three things happen inside the block:

  • The window stays open — each m.form() / m.confirm() / … reuses the same window rather than creating a new one.
  • print() is redirected into the UI instead of the terminal.
  • A non-interactive TTY (e.g. a script launched from a desktop shortcut) becomes interactive where possible (TextInterface).

When the block exits, stdout is restored. Any output that was buffered but not yet shown in the window is reprinted to the real terminal.

See Dialog methods for the full list of available dialogs.

Two ways to call run()

As config — pass a dataclass; read the parsed values off m.env:

m = run(Config)
print(m.env.my_number)

As subcommands — pass a list of Command subclasses. run() parses the CLI, selects the one subcommand, and calls its .run() for you; .env holds that chosen instance:

from dataclasses import dataclass
from mininterface import run
from mininterface.cli import Command

@dataclass
class Build(Command):
    target: str = "release"
    def run(self):
        print("building", self.target)

@dataclass
class Deploy(Command):
    host: str
    def run(self):
        print("deploying to", self.host)

run([Build, Deploy])        # ./app.py build --target debug  ->  Build.run()

Both modes are headless-safe by default (ask_on_empty_cli=False): a fully-defaulted invocation runs straight through with no prompt, so one file serves cron and an interactive user. Subcommands can share options by inheriting a common Command parent.

IDE suggestions

The immediate benefit is the type suggestions provided by your IDE.

Dataclass showcase

Imagine the following code:

from dataclasses import dataclass
from mininterface import run

@dataclass
class Env:
    my_paths: list[Path]
    """ The user is forced to input Paths. """


@dataclass
class Dialog:
    my_number: int = 2
    """ A number """

Now, accessing the main env will trigger the hint. Suggestion run

Calling form with an empty parameter will trigger editing the main env.

Suggestion form

Passing a dict will return the dict too.

Suggestion form

Passing a dataclass type causes it to be resolved.

Suggestion dataclass type

Should you have a resolved dataclass instance, pass it in.

Suggestion dataclass instance

As you see, its attributes are hinted alongside their description.

Suggestion dataclass expanded

Should the dataclass be hard for the IDE to investigate (e.g. due to a required field), just annotate the output.

Suggestion annotation possible

Select showcase

We aim for intuitive type inference everywhere. Here is an example with the m.select dialog.

from mininterface import run

m = run()
# x = m.select([1, 2, 3], default=2)  # -> int
# x = m.select([1, 2, 3], multiple=True)  # -> list[int]
# x = m.select([1, 2, 3], default=[2])  # -> list[int]

By default, the inferred type is an int.

Suggestion select

When you flag the selection as multiple, or when you submit multiple default values...

Suggestion select

...your IDE sees a list instead of a single value, so you can directly append to it etc.

Suggestion select

Nested configuration

You can easily nest the configuration. (See also Tyro Hierarchical Configs.)

Just put another dataclass inside your main one:

@dataclass
class FurtherConfig:
    token: str
    host: str = "example.org"

@dataclass
class Env:
    further: FurtherConfig

...
m = run(Env)
print(m.env.further.host)  # example.org

The attributes can be set via the CLI:

$./program.py --further.host example.net

Or via a YAML config file. Note that you are not obliged to define all the attributes; a subset will do. (E.g. you do not need to specify token.)

further:
  host: example.com

Bash completion

Run your program through the bundled mininterface executable to start a tutorial that installs bash completion.

$ mininterface integrate ./program

Bash completion

System dialog toolkit

Mininterface can be used as a standalone dialog layer for sh scripts. See mininterface --help.

$ mininterface select one two  # outputs a chosen item

Select dialog