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.3.tar.gz (13.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.3-py3-none-any.whl (16.8 kB view details)

Uploaded Python 3

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

Hashes for clemui-0.1.3.tar.gz
Algorithm Hash digest
SHA256 c98ffd902bb7ae472227410a4642c8e1258549c1ba50f849037615e3899aec31
MD5 b58d926c5a24a2b9cbdbf81b33387c8e
BLAKE2b-256 e60355e664dbe6001039a5071c5e48ea2c437fff249ccfc9926c69a3a85135b6

See more details on using hashes here.

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

Hashes for clemui-0.1.3-py3-none-any.whl
Algorithm Hash digest
SHA256 399253fbffe8f63aeac39fbd53eafe4116c7e750e6873e44019b23de19f12bfd
MD5 4434e1c5f39b34bcb0d1324993b0d0d8
BLAKE2b-256 c5ae0c3930dd508903f6e6e1c085227e5a73da2b8b5e9b862e642b3833233c3d

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