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
@componentdecorator - Class
Componentwith__init__()/setup()andrender() Props, where every attribute is an accessor- first-class component children through
props.children - transparent
Fragment(...)nodes for returning multiple children - widget namespaces:
tkfor classic Tk widgets andttkfor 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(), andstyle.component(). - some control flow:
Show,For,Switch/Match,Index,Dynamic - context:
create_context(),Provider,use_context() - stores:
create_store()for immutable updates andcreate_mutable()for Solid-style mutable object state - resources:
create_resource()withloading,error,state,mutate,refetch - lifecycle helpers:
create_effect(),on_mount(),on_cleanup() create_root()and explicit disposal through the returnedMount
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
Release history Release notifications | RSS feed
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 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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
83e8c7277ec8fd5652c3d890b6398bf2ef81a577585fb6391c34d8606ed9dc4b
|
|
| MD5 |
94d92f96824bb10e1234f33ceb3f0bfb
|
|
| BLAKE2b-256 |
263a4a61e540d7808fcd6b8268a3b6e959057c38e48e8fcadebe5192039da275
|
Provenance
The following attestation bundles were made for taut_tk-0.1.0.tar.gz:
Publisher:
release.yml on Aweptimum/taut.tk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
taut_tk-0.1.0.tar.gz -
Subject digest:
83e8c7277ec8fd5652c3d890b6398bf2ef81a577585fb6391c34d8606ed9dc4b - Sigstore transparency entry: 2123541846
- Sigstore integration time:
-
Permalink:
Aweptimum/taut.tk@5af6c8ab84a194653b705838f93d3c435503eebc -
Branch / Tag:
refs/tags/0.1.0 - Owner: https://github.com/Aweptimum
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5af6c8ab84a194653b705838f93d3c435503eebc -
Trigger Event:
release
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fd1c4494acec7aab67ed202af61987d2aa9cdaa5e1d3f2b48e14e362413ce7ad
|
|
| MD5 |
920630b811426d9a1d86ecaa5f594c19
|
|
| BLAKE2b-256 |
4adef084432aef3041a5e43916096fc6874d2b02355520ba2b31f2b3d1714a78
|
Provenance
The following attestation bundles were made for taut_tk-0.1.0-py3-none-any.whl:
Publisher:
release.yml on Aweptimum/taut.tk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
taut_tk-0.1.0-py3-none-any.whl -
Subject digest:
fd1c4494acec7aab67ed202af61987d2aa9cdaa5e1d3f2b48e14e362413ce7ad - Sigstore transparency entry: 2123541868
- Sigstore integration time:
-
Permalink:
Aweptimum/taut.tk@5af6c8ab84a194653b705838f93d3c435503eebc -
Branch / Tag:
refs/tags/0.1.0 - Owner: https://github.com/Aweptimum
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5af6c8ab84a194653b705838f93d3c435503eebc -
Trigger Event:
release
-
Statement type: