cmdgui
Simple terminal GUIs in Python. Draw your layout as text, fill in the widgets, and keep writing your program — the view runs on its own thread, so there's no event loop to hand control to and no draw calls to make.
┌─ name ───────────────────┐
│quinn │[ Greet ] ON fast
├─ fruit ──────────────────┼─ scores ──────────────────────────────────┐
│ apple │name score city │
│ banana │Ada 98 London │
│ cherry │Linus 87 Helsinki │
│ ├─ log ─────────────────────────────────────┤
│ │Hello, quinn! 👋 │
│ │picked cherry │
└──────────────────────────┴───────────────────────────────────────────┘
████████████████████████████████░░░░░░░░░░░░░░░░ 67%[ ] done
No dependencies. macOS and Linux; Windows support is written but untested.
Install
pip install cmdgui
Quick start
from cmdgui import View, Button, Stdout
import time
class App(View):
layout = """
start pause .
log - -
"""
start = Button("Start", on_click=lambda: print("started"))
pause = Button("Pause", on_click=lambda: print("paused"))
log = Stdout()
view = App()
for i in range(100):
print(f"tick {i}") # shows up in the log box
time.sleep(0.5)
Press q to quit (or Ctrl+C). Because the widgets are class attributes, your
editor knows view.start is a Button — autocomplete and type checking work.
Three ways to build a view
They can be mixed freely:
from cmdgui import View, Label
# 1. A subclass: everything in one place, fully typed in your editor
class App(View):
layout = "heading \n stdout"
heading = Label("Hello", align="center")
view = App()
# 2. Widgets passed in: quick one-off views
view = View("heading \n stdout", heading=Label("Hello", align="center"))
# 3. Types in the layout, configured afterwards
view = View("label[heading] \n stdout")
view.heading.set(text="Hello", align="center")
A keyword argument overrides a subclass's widget of the same name. Each view made
from a subclass gets its own copies of the widgets. For typed access to widgets
declared in the layout string, use view.get("heading", Label).
Layouts
Each word is a grid cell. Write type[name] to create a widget:
| Token | Meaning |
|---|---|
button[start] |
a button widget named start |
heading |
the widget you passed in (or declared on the class) as heading |
stdout |
otherwise: a widget whose name is its type |
start |
the existing widget start again — it spans into this cell |
- |
same as the cell to the left |
| |
same as the cell above |
. |
empty cell |
{b} / {nb} |
on the first cell of a widget: force a border on / off |
Every widget has to cover a rectangle. Get widgets with view.start or view["start"].
Sizes come from the widgets: a button wants to be 1 line tall, so its row is 1 line tall; everything else shares the remaining space. If the terminal is too small for the layout's minimum size, the view shows a message until it's resized.
Borders are drawn by the view, and neighbours share a single line with the
right junctions (├ ┬ ┼). The border title is the widget's name, or its title.
The focused widget's border is highlighted.
Widgets
| Type | Class | What it does |
|---|---|---|
text |
Text |
Wrapped text. text, align (left/center/right) |
label |
Label |
One line of text |
button |
Button |
text, on_click(fn). Click, or Enter/Space when focused |
text_input |
TextInput |
value, placeholder, on_submit(fn), on_change(fn) |
progress_bar |
ProgressBar |
value from 0 to 1 |
checkbox |
Checkbox |
text, checked, on_change(fn) |
toggle |
Toggle |
An on/off switch, same API as Checkbox |
menu |
Menu |
items, selected, on_select(fn(index, item)) |
table |
Table |
columns, rows. Scroll with the mouse wheel |
stdout |
Stdout |
Everything printed, stderr in red. Scroll with the mouse wheel. clear() |
Every widget also takes border, title, preferred_width and preferred_height.
The first positional argument is the main content: Label("text"), Menu(items),
Table(columns), ProgressBar(0.5). Callbacks can be passed in (on_click=) or set
later (button.on_click(fn)).
Setting an attribute redraws the widget: view.bar.value = 0.5 just works, and
widget.set(text=..., align=...) changes several at once. If you change a list in
place (view.menu.items.append(...)), call widget.refresh().
Popups
A popup floats over the layout. It has its own layout and widgets, just like a view, and sizes itself to fit them:
from cmdgui import View, Popup, Label, Button, Stdout
class Confirm(Popup):
layout = """
message -
yes no
"""
title = "Clear the log?"
message = Label("This can't be undone.")
yes = Button("Clear")
no = Button("Cancel")
def init(self): # wire up the popup's own widgets
self.no.on_click(self.close)
class App(View):
layout = "clear \n log"
clear = Button("Clear log", on_click=lambda: view.show(view.confirm))
log = Stdout()
confirm = Confirm() # popups can live on the view too
view = App()
view.confirm.yes.on_click(lambda: (view.log.clear(), view.confirm.close()))
view.show(popup)opens it in the middle, or usebelow=widget,above=widgetorat=(x, y). It flips to the other side if there's no room.popup.close(),popup.is_open,popup.on_close(fn).view.alert("Saved!", title="Done")shows a message with an OK button.- A popup with one widget doesn't need a layout:
Popup(Menu(items)), or a subclass with a single widget.
Settings, as class attributes or constructor arguments:
| Setting | Default | |
|---|---|---|
modal |
True |
blocks clicks and focus for everything underneath |
close_on_escape |
True |
|
close_on_outside_click |
False |
for a modal popup the click just closes it; otherwise it goes through too |
keep_typing |
False |
the focused text box keeps getting typed text, while arrows and Enter go to the popup (autocomplete, command menus) |
border, title |
True, None |
|
width, height |
None |
outer size; None fits the content |
Inside a popup, a widget's border title is only shown if you set its title.
examples/simple_console.py has a / command menu and examples/popups.py has a dialog and a dropdown.
Keys and focus
- Tab / Shift+Tab or a click moves focus between widgets that take input.
- Key presses go to the focused widget; typed characters go to a focused text box first.
view.on_key("ctrl+s", save)binds a key. Key names: letters,enter,escape,tab,backspace,delete,up/down/left/right,home/end,page_up/page_down,ctrl+a,alt+x,ctrl+up, ...View(layout, quit_key="q")sets the quit key;quit_key=Noneturns it off.
Ending the program
with View("...") as view:
...
view.wait() # blocks until q, view.quit() or Ctrl+C
Without wait(), the quit key ends your program as if it had finished.
If anything raises — in your code or inside a callback — the terminal is put back
to normal first, so the traceback is visible.
Your own widgets
from cmdgui import Widget, View, field
class Clock(Widget):
time: str = field(default="", kw_only=False) # fields become constructor arguments
show_seconds: bool = True
history: list = field(default_factory=list) # a fresh list for each clock
preferred_height = 1 # 5, "5+" (at least), "5-10" (between), or None
border = True
def init(self): # other setup (not constructor arguments)
self.ticks = 0
def draw(self, c): # c is a Canvas exactly the widget's size
c.text(0, 0, self.time)
def on_input(self, input): # mouse, stdout, and keys (while focused)
pass
view = View("clock") # registered automatically as "clock"
view = View("now", now=Clock("12:00"))
Annotated attributes are fields: keyword arguments by default, positional with
field(kw_only=False). Editors autocomplete and type-check them like a dataclass.
Override content_size() to return the (width, height) your content wants, so
popups can size themselves around it (None for either means "don't care").
Canvas has put, text, fill and border; self.mouse_pos() and
self.mouse_over() give the mouse relative to the widget; self.theme("key")
gets a style from the theme.
Themes
from cmdgui import View, style
view = View("...", theme={
"border_focus": style(fg="magenta", bold=True),
"selected": style(fg="black", bg="yellow"),
})
See DEFAULT_THEME in cmdgui.widgets for every key.
Metadata
Release files for cmdgui 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| cmdgui-0.1.0.tar.gz | 34.9 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| cmdgui-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 68.5 kB
Release files / cmdgui-0.1.0.tar.gz
| Download URL | cmdgui-0.1.0.tar.gz |
|---|---|
| Size | 34.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6ed92467e1d18a630517957590215562a84cf531a47ea5a6e85f8622551dbf01
|
|
BLAKE2b-256 checksum How to use checksums |
54facbc1073031c4755ac63af10b3ee1fc22d206d08350294fa2e279a21b4690
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.4
|
Release files / cmdgui-0.1.0-py3-none-any.whl
| Download URL | cmdgui-0.1.0-py3-none-any.whl |
|---|---|
| Size | 33.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
bf939e01e6994e7b1725b2dbc8117ee1c6f665ca1b14251e6efaedd103708d0f
|
|
BLAKE2b-256 checksum How to use checksums |
255f3ea1373a9516edad9a5237477d0a8ab43583090a3cc0323237c2c59dc12d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.4
|
Security for cmdgui 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.