A reactive tkinter-based desktop UI library
Project description
clemui
A reactive tkinter-based desktop UI library for Python. No external dependencies — pure Python + stdlib tkinter.
pip install clemui
Quick Start
from clemui import App, Signal, Component, VNode, VBox
from clemui.widgets import Label, Button
class Counter(Component):
def __init__(self):
super().__init__()
self.count = Signal(0)
def render(self):
return VNode(VBox, spacing=10, children=[
VNode(Label, text=f"Count: {self.count.value}"),
VNode(Button, text="+", command=lambda: self.count.set(self.count.value + 1)),
])
app = App(title="Counter")
app.mount(Counter())
app.run()
Core Concepts
Signals
Signal is a reactive value container. When its value changes, all bound widgets and subscribed components update automatically.
name = Signal("Alice")
age = Signal(30)
name.value # "Alice"
name.set("Bob") # triggers updates everywhere
Components
Components are class-based UI units. Implement render() to return a tree of VNodes.
class MyComponent(Component):
def __init__(self):
super().__init__()
self.title = Signal("Hello")
def render(self):
return VNode(VBox, children=[
VNode(Label, text=self.title),
])
def did_mount(self):
# called after the widget tree is created
pass
def did_unmount(self):
# called before the component is destroyed
pass
VNodes
A VNode(type, props, children) describes a widget declaratively:
VNode(Button, text="Click", command=handler, state='disabled')
VNode(HBox, spacing=8, children=[
VNode(Label, text="Name:"),
VNode(Entry, text=signal),
])
Properties that are Signal instances are auto-bound — the widget updates whenever the signal changes.
Widgets Reference
| Widget | Import | Key Props |
|---|---|---|
| Label | clemui.widgets.Label |
text, fg, font, anchor |
| Button | clemui.widgets.Button |
text, command, state, bg |
| Entry | clemui.widgets.Entry |
text (Signal), width, font, show |
| Text | clemui.widgets.Text |
state, height, width, fg, bg (has scrollbar) |
| Frame | clemui.widgets.Frame |
height, width, bg, name (sets _custom_name) |
| Canvas | clemui.widgets.Canvas |
image (PIL.Image), bg, cursor |
| Slider | clemui.widgets.Slider |
value (Signal), from_, to, orient, showvalue |
| Checkbox | clemui.widgets.Checkbox |
text, variable (Signal), state |
| ComboBox | clemui.widgets.ComboBox |
values (list/Signal), value (Signal), state |
| Treeview | clemui.widgets.Treeview |
columns, show |
| Menu | clemui.widgets.Menu |
tearoff, children (list of VNodes) |
Text (Multi-line)
The Text widget wraps a tk.Text with a scrollbar. Use get_text() / set_text() to access content.
VNode(Text, state='normal', height=8, width=60, wrap='word')
Layout Helpers
from clemui import VBox, HBox, Padding
VBox(spacing=4) # vertical stack
HBox(spacing=8) # horizontal stack
Padding(padx=8, pady=4) # padding wrapper
Each accepts pack options for tkinter geometry management:
VNode(VBox, spacing=8, pack={'fill': 'both', 'expand': True})
VNode(Label, text="A", pack={'side': 'left', 'fill': 'x'})
Examples
Counter with Reset
class Counter(Component):
def __init__(self):
super().__init__()
self.count = Signal(0)
self.step = Signal(1)
def render(self):
return VNode(VBox, spacing=8, pack={'padx': 16, 'pady': 16}, children=[
VNode(Label, text=f"Count: {self.count.value}",
font=("Courier New", 18, "bold")),
VNode(HBox, spacing=4, children=[
VNode(Button, text="-", command=lambda: self.count.set(self.count.value - self.step.value)),
VNode(Button, text="Reset", command=lambda: self.count.set(0)),
VNode(Button, text="+", command=lambda: self.count.set(self.count.value + self.step.value)),
]),
VNode(HBox, spacing=4, children=[
VNode(Label, text="Step:"),
VNode(Slider, from_=1, to=10, orient="horizontal",
value=self.step, showvalue=True),
]),
])
Todo List
class TodoApp(Component):
def __init__(self):
super().__init__()
self.items = Signal([])
self.input = Signal("")
def render(self):
return VNode(VBox, spacing=6, pack={'padx': 12, 'pady': 12}, children=[
VNode(Label, text="TODO", font=("Courier New", 14, "bold")),
VNode(HBox, spacing=4, children=[
VNode(Entry, text=self.input, width=30),
VNode(Button, text="Add", command=self._add),
]),
VNode(Label, text="\n".join(f"• {i}" for i in self.items.value)
if self.items.value else "(empty)",
font=("Courier New", 10)),
])
def _add(self):
if self.input.value.strip():
self.items.set(self.items.value + [self.input.value.strip()])
self.input.set("")
Theme Toggle
from clemui.theme import theme
class ThemedApp(Component):
def render(self):
m = theme.mode
return VNode(VBox, children=[
VNode(Label, text=f"Mode: {m}",
fg=theme.primary, font=("Courier New", 12)),
VNode(Button, text="☀" if m == "dark" else "☾",
command=self._toggle),
])
def _toggle(self):
theme.toggle_mode()
self._app.root.configure(bg=theme.window_bg)
self.update()
Form with Dialogs
from clemui import dialogs
class Form(Component):
def __init__(self):
super().__init__()
self.status = Signal("Click a button")
def render(self):
return VNode(VBox, spacing=6, children=[
VNode(Button, text="Open Image", command=lambda: self._open()),
VNode(Button, text="Save File", command=lambda: self._save()),
VNode(Label, text=self.status),
])
def _open(self):
path = dialogs.open_image()
if path:
self.status.set(f"Opened: {Path(path).name}")
def _save(self):
path = dialogs.save_text(initialfile="out.txt")
if path:
Path(path).write_text("hello")
self.status.set(f"Saved to: {Path(path).name}")
Available dialogs:
| Function | Purpose |
|---|---|
open_image() |
Open image file picker |
open_text() |
Open text file picker |
save_image(initialfile=...) |
Save image dialog |
save_audio(initialfile=...) |
Save audio dialog |
save_text(initialfile=...) |
Save text dialog |
prompt(title, label, default) |
Simple text input prompt |
Clock (Reactive Updates)
import time
class Clock(Component):
def __init__(self):
super().__init__()
self.now = Signal("")
self._tick()
def render(self):
return VNode(Label, text=f"🕐 {self.now.value}",
font=("Courier New", 24))
def _tick(self):
self.now.set(time.strftime("%H:%M:%S"))
if self._mounted:
self._app.root.after(1000, self._tick)
Theme System
from clemui.theme import theme
theme.mode # "dark" or "light"
theme.toggle_mode() # switch between them
theme.set_mode("light")
# Color tokens (swap between modes)
theme.primary # button bg, label fg
theme.secondary # accent
theme.tertiary # dark red accent
theme.neutral # window bg, frame bg
theme.grey # status text, slider trough
Dark mode palette:
- Background:
#E9E1D4(warm beige) - Text:
#2E2E2E(dark grey) - Accents: gold
#B5A642, red#8C2727
Building Custom Components
class Card(Component):
def __init__(self, title="", description=""):
super().__init__()
self._title = title
self._desc = description
def render(self):
return VNode(Frame, bg=theme.primary, pack={'padx': 6, 'pady': 6}, children=[
VNode(VBox, spacing=4, children=[
VNode(Label, text=self._title,
font=("Courier New", 12, "bold"), fg=theme.neutral),
VNode(Label, text=self._desc,
font=("Courier New", 10), fg=theme.neutral),
]),
])
# Usage in a parent component:
VNode(HBox, children=[
VNode(Card, title="Photo", description="Convert photos"),
VNode(Card, title="TTS", description="Text to speech"),
])
Tips
- Always call
super().__init__()in your component's__init__ - Use
self._app.rootto access the root tkinter window update()triggers a full re-render — use it when you need to rebuild the widget tree- Signals can be passed as widget props for automatic two-way binding
- Use
pack={'fill': 'both', 'expand': True}on the root VNode to fill the window Component.bind(signal)subscribes to a signal and callsupdate()on change
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
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 clemui-0.1.3.tar.gz.
File metadata
- Download URL: clemui-0.1.3.tar.gz
- Upload date:
- Size: 13.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c98ffd902bb7ae472227410a4642c8e1258549c1ba50f849037615e3899aec31
|
|
| MD5 |
b58d926c5a24a2b9cbdbf81b33387c8e
|
|
| BLAKE2b-256 |
e60355e664dbe6001039a5071c5e48ea2c437fff249ccfc9926c69a3a85135b6
|
File details
Details for the file clemui-0.1.3-py3-none-any.whl.
File metadata
- Download URL: clemui-0.1.3-py3-none-any.whl
- Upload date:
- Size: 16.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
399253fbffe8f63aeac39fbd53eafe4116c7e750e6873e44019b23de19f12bfd
|
|
| MD5 |
4434e1c5f39b34bcb0d1324993b0d0d8
|
|
| BLAKE2b-256 |
c5ae0c3930dd508903f6e6e1c085227e5a73da2b8b5e9b862e642b3833233c3d
|