Prompts, pickers and progress bars for the terminal. No dependencies, just the standard library.
Widgets draw in place and redraw over the lines they own. Once a question is answered it collapses to a single line and stays there, so a long run of prompts reads back like a transcript rather than a scrolling mess.
It is not on PyPI. Depend on it by git reference:
dependencies = [
"cli-widgets @ git+https://github.com/KontrolQ/cli-widgets@v0.1.0",
]Requires Python 3.11 or newer. Works on Windows, macOS and Linux.
from cli_widgets.widgets import entry
name = entry.ask("Name for this machine", default="win98se")Pass limit to cap the length, placeholder for greyed-out hint text, and
validate for a function that returns a complaint string when the value is no
good, or None when it is fine:
def not_empty(text):
if not text.strip():
return "That cannot be blank."
owner = entry.ask("Owner name", validate=not_empty)Left, right, home, end, backspace and delete all work while typing.
from cli_widgets.widgets import choice
size = choice.ask("System disk", ["2 GB", "4 GB", "8 GB"], chosen=1)chosen is where the pointer starts. The list scrolls if it does not fit and
shows a counter when it scrolls.
Options do not have to be plain strings. A tuple or a dict gives you a second column of dim text beside the label:
choice.ask("System disk", [
("4 GB", ""),
("160 GB", "needs a large disk patch"),
])
choice.ask("Guest", [
{"label": "Windows 98 SE", "note": "windows-9x"},
])from cli_widgets.widgets import checklist
updates = checklist.ask("Updates", ["IE6 SP1", "DirectX 9.0c", "KernelEx"], ticked=[0])Space ticks and unticks, enter accepts. ticked takes the positions that
start ticked. You get back the options themselves, not their positions.
from cli_widgets.widgets import confirm
if confirm.ask("Build it"):
build()Pass affirmed_by_default=False to start on No.
from cli_widgets.widgets import browser
folder = browser.ask_folder("Where should it be created", ".")
disc = browser.ask_file("Installation disc", ".", (".iso", ".img"))Up and down move, enter opens a folder or chooses a file, left goes up a level.
On Windows the drive letters appear once you reach the top. Files show their
size. Leave suffixes off to accept anything. Both return None if nothing
was chosen.
from cli_widgets.widgets import progress
bar = progress.new("Copying", len(files))
for file in files:
copy(file)
bar = progress.step(bar, 1, caption=file.name)
progress.finish(bar)step advances the bar and redraws it. The caption is optional and only
changes when you pass one.
from cli_widgets.widgets import summary
summary.show("Ready to build", [
("Machine", "win98se"),
("Memory", "256 MB"),
("System disk", "4 GB"),
])Labels line up in a column with dotted leaders.
For longer flows you can hold the alternate screen and pin a header above the prompts:
from cli_widgets.widgets import prompting
prompting.take_screen()
prompting.begin(["Windows 98 Second Edition"])
name = entry.ask("Name for this machine")
prompting.repin([f"Windows 98 Second Edition {name}"])
...
prompting.end()begin sets the pinned lines and clears the answers below them, repin
replaces the header without touching the answers, and end gives the screen
back. Without take_screen the widgets just print normally, which is what you
want when the output is being piped somewhere.
Ctrl+C raises KeyboardInterrupt from any widget.
Box drawing, arrows and ticks are used when the terminal encoding can represent them, and plain ASCII when it cannot. Nothing needs configuring.
uv sync --group dev
uv run pytest
uv run ruff check .
uv run mypy cli_widgets
uv run pyrightTests live in tests/ and are named *.test.py. Coverage has to stay at
100 percent.