Skip to main content

Add your description here

Project description

taut.tk

A small SolidJS-inspired runtime for Tkinter using reaktiv for fine-grained signals.

This is currently a prototype that I hope makes Tkinter more fun

from typing import Protocol

from taut import component
from taut import layout
from taut import tk
from taut.reactive import Accessor
from taut.reactive import Mutator
from taut.reactive import create_signal
from taut.runtime import create_root
from taut.control import For
from taut.control import Show


class CounterProps(Protocol):
    label: Accessor[str]
    count: Accessor[int]
    set_count: Mutator[int]


@component
def counter(props: CounterProps):
    todos, set_todos = create_signal(["wire props", "own effects", "dispose cleanly"])

    return layout.VStack(
        tk.Label(text=lambda: f"{props.label()}: {props.count()}"),
        tk.Button(text="Increment", on_click=lambda: props.set_count(lambda n: n + 1)),
        Show(
            lambda: props.count() % 2 == 0,
            lambda: tk.Label(text="Even"),
            fallback=lambda: tk.Label(text="Odd"),
        ),
        For(todos, lambda item: tk.Label(text=item), key=lambda item: item),
        layout.HStack(
            tk.Button(text="-", on_click=lambda: set_todos(lambda items: items[:-1])),
            gap=6,
        ),
        padding=12,
        gap=6,
    )

count, set_count = create_signal(0)
mount = create_root(
    lambda: counter(label="Solid TK", count=count, set_count=set_count),
    title="Solid TK",
)
mount.widget.mainloop()

Why Tk?

I keep reaching for it every time I want a small UI at work and I keep getting bogged down in abstractions I think are loose but turn out to be more tightly-coupled than I realize. Imperatively updating state is also a chore and makes for widgets with lots of clunky helpers, and I find widget vars unwieldy. My most recent attempt I threw reaktiv at the problem and was surprised at how streamlined it made my widgets. I felt it could be more. So here we are.

What's Here So Far

  • Functional components using @component decorator
  • Class Component with __init__()/setup() and render()
  • Props, where every attribute is an accessor
  • first-class component children through props.children
  • transparent Fragment(...) nodes for returning multiple children
  • widget namespaces: tk for classic Tk widgets and ttk for themed widgets
  • layout namespace: layout.VStack, layout.HStack, layout.Grid, layout.Item, layout.GridItem
  • StyleX-ish style objects agnostic to tk/ttk widgets with style.define(), style.merge(), and style.component().
  • some control flow: Show, For, Switch / Match, Index, Dynamic
  • context: create_context(), Provider, use_context()
  • stores: create_store() for immutable updates and create_mutable() for Solid-style mutable object state
  • resources: create_resource() with loading, error, state, mutate, refetch
  • lifecycle helpers: create_effect(), on_mount(), on_cleanup()
  • create_root() and explicit disposal through the returned Mount

Doc Topics

  • Context: providers, typed context helpers, and forwarded children.
  • Control: conditional rendering.
  • Reactive primitives: signals, memos, effects, and owner cleanup.
  • Resources: worker-thread loading, refreshes, mutation, and status.
  • Scheduling: queueing work
  • Stores: state management
  • Style: StyleX-ish style objects, merging, and component styling.
  • Widgets: Tkinter primitives

Examples

The runnable examples live in examples. Start with the examples README for a short path through them.

Strong Typing

To follow Solid's model of named args collected into reactive props, there is some brittle trickery.

In JS/TS land, there's tons of tooling around jsx/tsx. Python doesn't have that.
What it does have are .pyi files. We can define parameter transformations that type checkers can't inspect and then give them the publc facing API with a wink and a smile. The problem is generating those .pyi files.

The beginning of solving that problem is the @component decorator. In addition to transforming functions into rendered nodes, is also a syntax marker that can be inspected.

The next bit is stub-genning; basically a build step. Files in the working directory are scanned for component declarations and a corresponding .pyi file is generated, unrolling the typed prop object into a function signature.

This process can be manual with uv run taut.tk stubs ., or you can run a watcher to do it live with uv run taut.tk watch (it might be brittle though).

There are some caveats though, as this bit is honestly harder and more involved than the actual framework to get right. It imposes some limitations on import patterns.

When a component module imports sibling modules, prefer from . import <module> or a direct submodule import like import examples.todo.module. Relying on imports that use __init__.py, like from examples.todo import module, can route through generated package stubs instead of the source module. This is a limitation imposed by the current state of type checkers, and the only real work around that preserves usage of __init__.py is stubbing everything. Not really a feasible thing to develop or maintain right now.

Design Notes

create_signal() returns an accessor and a mutator:

from taut.reactive import create_signal
count, set_count = create_signal(0)
count()
set_count(lambda value: value + 1)

The accessor is still compatible with reaktiv signals, so widgets can bind to it directly. Writable widgets such as tk.Entry(value=..., on_input=...) receive the accessor and mutator separately.

Component.__new__ returns a renderable node to keep the class API simple

Counter(title="Solid TK")

Inside a component, self.props.title() reads a reaktiv signal. This is intentionally accessor-oriented.

Existing signals are preserved, while plain values and callbacks are wrapped as signal values. Tk widget props use one additional convention: callable non-event props are treated as reactive bindings, and event props such as on_click / command are passed through as callbacks.

tk.Label(text=f"{count()}")                  # snapshot now
tk.Label(text=count)                         # reactive signal value
tk.Label(text=lambda: f"{count()}")          # reactive derived expression

That means a component prop can be read inside a derived expression:

tk.Label(text=lambda: f"Hello {self.props.name()}")

or forwarded directly to a widget prop:

tk.Label(text=self.props.name)

Component children are also collected into props.children, so container-like components can feel like Solid components instead of manually passing a children= prop:

@component
def panel(props):
    return layout.VStack(
        tk.Label(text=props.title),
        props.children(),
        padding=8,
    )

panel(tk.Label(text="Nested"), title="Details")

Control-flow nodes and Fragment(...) are transparent to layout. They produce children; the parent widget or layout helper decides where those children go:

@component
def rows(props):
    return For(props.items, lambda item: tk.Label(text=item), key=lambda item: item)

layout.VStack(
    tk.Label(text="Before"),
    rows(items=todos),
    tk.Label(text="After"),
)

In that example, the repeated labels are laid out by VStack between Before and After; For and the component do not create wrapper frames.

Benchmarks

To estimate framework overhead without needing a display server, run the fake-Tk benchmark:

uv run python benchmarks/bench_overhead.py

It compares raw Tk-style widget creation with taut.tk mounting for static labels, reactive label props, and a component wrapper. The most useful line is the reported extra microseconds per widget, because native Tk/Tcl startup and platform display behavior are intentionally excluded.

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

taut_tk-0.1.1.tar.gz (34.6 kB view details)

Uploaded Source

Built Distribution

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

taut_tk-0.1.1-py3-none-any.whl (43.1 kB view details)

Uploaded Python 3

File details

Details for the file taut_tk-0.1.1.tar.gz.

File metadata

  • Download URL: taut_tk-0.1.1.tar.gz
  • Upload date:
  • Size: 34.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for taut_tk-0.1.1.tar.gz
Algorithm Hash digest
SHA256 ebf4fb896837c09b89839e081137a43ebf4e246a411561e7b2ec7bbf4f0e1557
MD5 128d71b3d4cf062aa957bbcdafe42f60
BLAKE2b-256 d409e5dea500cbdfa27f651d72aef813f4e8152dd9846e6ebdc0f59d2e595054

See more details on using hashes here.

Provenance

The following attestation bundles were made for taut_tk-0.1.1.tar.gz:

Publisher: release.yml on Aweptimum/taut.tk

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file taut_tk-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: taut_tk-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 43.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for taut_tk-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 c9b0f2930e82edf2cf696495548857bdda4401cc45962e1d8e3f450614463d52
MD5 407bec720768910b350b904006e61078
BLAKE2b-256 3fa35e419d4edb44685535c721357d118428afd257543453c5e957afb24d11f9

See more details on using hashes here.

Provenance

The following attestation bundles were made for taut_tk-0.1.1-py3-none-any.whl:

Publisher: release.yml on Aweptimum/taut.tk

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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