Skip to main content

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 tkintercreate 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:

  1. 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.
  2. Run phase (mainloop()): a Textual App subclass is generated on the fly. Its compose() walks the tree and instantiates the real Textual widgets; pack/grid are 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

  • pack is the robust path; grid is best-effort. For non-trivial layouts, nest Frames (just like idiomatic tkinter) rather than mixing sides in one frame.
  • Colors and sizes use Textual semantics (color names, "100%", "1fr"), not pixels.
  • An Entry bound to a textvariable fires its trace once on mount (Textual emits a Changed event when the Input first 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/gridStringVar/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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tuikinter-0.1.0.tar.gz (12.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tuikinter-0.1.0-py3-none-any.whl (11.0 kB view details)

Uploaded Python 3

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

Hashes for tuikinter-0.1.0.tar.gz
Algorithm Hash digest
SHA256 da09169cb7488274d92f5224d886fd071416973da13ddf6c8cdfb409fa406aac
MD5 5cf7ca30c3f4f1d975e961f736db5732
BLAKE2b-256 6dcd0c1dbbf156b90fb1940976841f68e46771034f700562433f8fe9666ddc62

See more details on using hashes here.

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

Hashes for tuikinter-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 152ef7756a8ddd58cb05c290312831670fbf63b37ecd676681301b865df04cc3
MD5 fa049d8531e0de80760a56a569841482
BLAKE2b-256 e9c272eb2c41adfb94d65bc0b08f9efe43c634df2ea2fbbb47b3d292f62ebb50

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page