Skip to main content

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.root to 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 calls update() 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

clemui-0.1.4.tar.gz (11.3 kB view details)

Uploaded Source

Built Distribution

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

clemui-0.1.4-py3-none-any.whl (16.8 kB view details)

Uploaded Python 3

File details

Details for the file clemui-0.1.4.tar.gz.

File metadata

  • Download URL: clemui-0.1.4.tar.gz
  • Upload date:
  • Size: 11.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for clemui-0.1.4.tar.gz
Algorithm Hash digest
SHA256 be5bd952b6a099eef8367247ac53eb6a4e2ce5b1c297de597e614ae587156f07
MD5 6383f0756024ef95d59bac62ef21e535
BLAKE2b-256 c9d32143f8c170074d4b0805798cffbcdc680dc69abe1fdb29bbdfaf6d1a917a

See more details on using hashes here.

File details

Details for the file clemui-0.1.4-py3-none-any.whl.

File metadata

  • Download URL: clemui-0.1.4-py3-none-any.whl
  • Upload date:
  • Size: 16.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for clemui-0.1.4-py3-none-any.whl
Algorithm Hash digest
SHA256 4babe42a1f91cc107950723c3e45a2f93d7f8cbef14927c146d80927b4633105
MD5 29ba18f4d0aacf3f75f79787f50748d9
BLAKE2b-256 8d7d7655ffe34312d4c4ca1205e09f37ab0e7b78a28a4a3e31eaab95ec5ba3ac

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