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.0.tar.gz (34.0 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.0-py3-none-any.whl (42.5 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: taut_tk-0.1.0.tar.gz
  • Upload date:
  • Size: 34.0 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.0.tar.gz
Algorithm Hash digest
SHA256 83e8c7277ec8fd5652c3d890b6398bf2ef81a577585fb6391c34d8606ed9dc4b
MD5 94d92f96824bb10e1234f33ceb3f0bfb
BLAKE2b-256 263a4a61e540d7808fcd6b8268a3b6e959057c38e48e8fcadebe5192039da275

See more details on using hashes here.

Provenance

The following attestation bundles were made for taut_tk-0.1.0.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.0-py3-none-any.whl.

File metadata

  • Download URL: taut_tk-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 42.5 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.0-py3-none-any.whl
Algorithm Hash digest
SHA256 fd1c4494acec7aab67ed202af61987d2aa9cdaa5e1d3f2b48e14e362413ce7ad
MD5 920630b811426d9a1d86ecaa5f594c19
BLAKE2b-256 4adef084432aef3041a5e43916096fc6874d2b02355520ba2b31f2b3d1714a78

See more details on using hashes here.

Provenance

The following attestation bundles were made for taut_tk-0.1.0-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