Write terminal UIs with a tkinter/PyQt-style imperative API, powered by Textual.
Project description
tuikinter
Write terminal UIs (TUIs) with a tkinter / PyQt-style imperative API — powered by Textual.
Textual is a fantastic, modern TUI framework, but its programming model is declarative and async: you yield widgets inside App.compose(), style them with TCSS, and manage state with reactives. If your mental model is tkinter — create widgets one by one, lay them out with pack / grid, then call mainloop() — tuikinter gives you exactly that, while Textual does the heavy lifting underneath.
import tuikinter as tk
root = tk.Tk()
root.title("Hello")
name = tk.StringVar(root, value="World")
tk.Label(root, text="Name:").pack(side="left")
tk.Entry(root, textvariable=name).pack(side="left", fill="x", expand=True)
tk.Button(root, text="Greet", command=lambda: print(f"Hi {name.get()}")).pack()
tk.Button(root, text="Quit", command=root.destroy).pack()
root.mainloop()
Why
If you already know tkinter, you already know tuikinter. The API mirrors it closely — same widget names, same pack/grid, same StringVar/IntVar/BooleanVar with trace_add, same command= callbacks, same widget.config(...) and widget["text"] = .... You get a real, keyboard-navigable terminal app (and, because it's Textual underneath, it can even run in a browser via textual serve).
Install
pip install tuikinter
Requires Python 3.9+. Textual is pulled in automatically.
How it works
A façade in two phases:
- Build phase (before
mainloop()): each widget you create is a lightweight spec proxy that records which Textual widget to use, its options, callbacks, and layout intent, attaching itself to its parent to form a tree. - Run phase (
mainloop()): a TextualAppsubclass is generated on the fly. Itscompose()walks the tree and instantiates the real Textual widgets;pack/gridare translated into containers (Vertical/Horizontal/Grid) plus styles; callbacks are routed back to your plain functions through Textual's message system.
This is the same trick tkinter itself uses behind the scenes with Tcl commands.
Widgets
| tuikinter | maps to (Textual) | notes |
|---|---|---|
Tk |
App |
root window; call .mainloop() |
Frame |
Vertical / Horizontal / Grid |
container; nest for complex layouts |
Label |
Static |
config(text=...) to update |
Button |
Button |
command=fn; variant="success"/"error"/... |
Entry |
Input |
textvariable=, get(), insert(), delete(), show="*" |
Text |
TextArea |
multi-line |
Checkbutton |
Checkbox |
variable=BooleanVar, command=fn |
Listbox |
OptionList |
values=[...], insert(), get_selected() |
Progressbar |
ProgressBar |
maximum=, set(), step() |
Variables
name = tk.StringVar(root, value="Joshua")
name.trace_add("write", lambda: print("changed to", name.get()))
name.set("World") # pushes to any bound Entry + fires traces
StringVar, IntVar, and BooleanVar bind two-way to Entry / Checkbutton.
Layout
pack() is the primary, robust layout manager:
tk.Label(root, text="Top").pack(side="top", fill="x")
tk.Button(root, text="Left").pack(side="left", padx=1)
tk.Entry(root).pack(side="left", fill="x", expand=True)
grid() is supported on a best-effort basis (terminal grid semantics differ from pixel grids):
grid = tk.Frame(root); grid.pack()
for i, t in enumerate("789456123"):
tk.Button(grid, text=t).grid(row=i // 3, column=i % 3)
tk.Button(grid, text="0").grid(row=3, column=0, columnspan=3)
Timers
root.after(1000, lambda: print("once, after 1s"))
root.every(1000, lambda: clock.config(text=time.strftime("%H:%M:%S")))
Limitations / honest notes
packis the robust path;gridis best-effort. For non-trivial layouts, nestFrames (just like idiomatictkinter) rather than mixingsides in one frame.- Colors and sizes use Textual semantics (color names,
"100%","1fr"), not pixels. - An
Entrybound to atextvariablefires its trace once on mount (Textual emits aChangedevent when theInputfirst appears). Usually harmless — worth knowing if your trace has side effects. - Dynamically creating + packing widgets after
mainloop()has started is best-effort.
Development
git clone https://github.com/joshuamaojh/tuikinter
cd tuikinter
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest
Tests drive the app headlessly with Textual's Pilot harness.
License
MIT © Joshua Mao
中文简述
tuikinter 让你用 tkinter / PyQt 那样的命令式写法来写终端 TUI,底层由 Textual 驱动。会 tkinter 就会它:控件名、pack/grid、StringVar/IntVar/BooleanVar(带 trace_add)、command= 回调、config() / widget["text"]=... 都一致。
import tuikinter as tk
root = tk.Tk()
tk.Label(root, text="你好").pack()
tk.Button(root, text="退出", command=root.destroy).pack()
root.mainloop()
几个要点:pack 最稳,复杂布局请像 tkinter 那样嵌套 Frame;颜色/尺寸走 Textual 语义(色名、"100%"、"1fr"),不是像素。
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tuikinter-0.1.0.tar.gz.
File metadata
- Download URL: tuikinter-0.1.0.tar.gz
- Upload date:
- Size: 12.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
da09169cb7488274d92f5224d886fd071416973da13ddf6c8cdfb409fa406aac
|
|
| MD5 |
5cf7ca30c3f4f1d975e961f736db5732
|
|
| BLAKE2b-256 |
6dcd0c1dbbf156b90fb1940976841f68e46771034f700562433f8fe9666ddc62
|
File details
Details for the file tuikinter-0.1.0-py3-none-any.whl.
File metadata
- Download URL: tuikinter-0.1.0-py3-none-any.whl
- Upload date:
- Size: 11.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
152ef7756a8ddd58cb05c290312831670fbf63b37ecd676681301b865df04cc3
|
|
| MD5 |
fa049d8531e0de80760a56a569841482
|
|
| BLAKE2b-256 |
e9c272eb2c41adfb94d65bc0b08f9efe43c634df2ea2fbbb47b3d292f62ebb50
|