taut.tk 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- taut_tk-0.1.0/PKG-INFO +215 -0
- taut_tk-0.1.0/README.md +202 -0
- taut_tk-0.1.0/pyproject.toml +35 -0
- taut_tk-0.1.0/src/taut/__init__.py +35 -0
- taut_tk-0.1.0/src/taut/cli/__init__.py +36 -0
- taut_tk-0.1.0/src/taut/cli/stub_model.py +161 -0
- taut_tk-0.1.0/src/taut/cli/stubs.py +393 -0
- taut_tk-0.1.0/src/taut/cli/watch.py +218 -0
- taut_tk-0.1.0/src/taut/component.py +114 -0
- taut_tk-0.1.0/src/taut/context.py +108 -0
- taut_tk-0.1.0/src/taut/control.py +493 -0
- taut_tk-0.1.0/src/taut/layout.py +205 -0
- taut_tk-0.1.0/src/taut/nodes.py +308 -0
- taut_tk-0.1.0/src/taut/props.py +119 -0
- taut_tk-0.1.0/src/taut/reactive.py +135 -0
- taut_tk-0.1.0/src/taut/resource.py +235 -0
- taut_tk-0.1.0/src/taut/runtime.py +466 -0
- taut_tk-0.1.0/src/taut/scheduler.py +52 -0
- taut_tk-0.1.0/src/taut/stores.py +490 -0
- taut_tk-0.1.0/src/taut/style.py +247 -0
- taut_tk-0.1.0/src/taut/tk.py +304 -0
- taut_tk-0.1.0/src/taut/tk_props.py +735 -0
- taut_tk-0.1.0/src/taut/ttk.py +272 -0
taut_tk-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: taut.tk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Author: Sam I
|
|
6
|
+
Author-email: Sam I <aweptimum@gmail.com>
|
|
7
|
+
Requires-Dist: reaktiv>=0.22.0
|
|
8
|
+
Requires-Dist: pillow>=12.2.0 ; extra == 'dev'
|
|
9
|
+
Requires-Dist: watchdog>=6.0.0 ; extra == 'dev'
|
|
10
|
+
Requires-Python: >=3.13
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# taut.tk
|
|
15
|
+
|
|
16
|
+
A small SolidJS-inspired runtime for Tkinter using
|
|
17
|
+
[reaktiv](https://pypi.org/project/reaktiv/) for fine-grained signals.
|
|
18
|
+
|
|
19
|
+
This is currently a prototype that I hope makes Tkinter more fun
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from typing import Protocol
|
|
23
|
+
|
|
24
|
+
from taut import component
|
|
25
|
+
from taut import layout
|
|
26
|
+
from taut import tk
|
|
27
|
+
from taut.reactive import Accessor
|
|
28
|
+
from taut.reactive import Mutator
|
|
29
|
+
from taut.reactive import create_signal
|
|
30
|
+
from taut.runtime import create_root
|
|
31
|
+
from taut.control import For
|
|
32
|
+
from taut.control import Show
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class CounterProps(Protocol):
|
|
36
|
+
label: Accessor[str]
|
|
37
|
+
count: Accessor[int]
|
|
38
|
+
set_count: Mutator[int]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@component
|
|
42
|
+
def counter(props: CounterProps):
|
|
43
|
+
todos, set_todos = create_signal(["wire props", "own effects", "dispose cleanly"])
|
|
44
|
+
|
|
45
|
+
return layout.VStack(
|
|
46
|
+
tk.Label(text=lambda: f"{props.label()}: {props.count()}"),
|
|
47
|
+
tk.Button(text="Increment", on_click=lambda: props.set_count(lambda n: n + 1)),
|
|
48
|
+
Show(
|
|
49
|
+
lambda: props.count() % 2 == 0,
|
|
50
|
+
lambda: tk.Label(text="Even"),
|
|
51
|
+
fallback=lambda: tk.Label(text="Odd"),
|
|
52
|
+
),
|
|
53
|
+
For(todos, lambda item: tk.Label(text=item), key=lambda item: item),
|
|
54
|
+
layout.HStack(
|
|
55
|
+
tk.Button(text="-", on_click=lambda: set_todos(lambda items: items[:-1])),
|
|
56
|
+
gap=6,
|
|
57
|
+
),
|
|
58
|
+
padding=12,
|
|
59
|
+
gap=6,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
count, set_count = create_signal(0)
|
|
63
|
+
mount = create_root(
|
|
64
|
+
lambda: counter(label="Solid TK", count=count, set_count=set_count),
|
|
65
|
+
title="Solid TK",
|
|
66
|
+
)
|
|
67
|
+
mount.widget.mainloop()
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Why Tk?
|
|
71
|
+
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.
|
|
72
|
+
|
|
73
|
+
## What's Here So Far
|
|
74
|
+
|
|
75
|
+
- Functional components using `@component` decorator
|
|
76
|
+
- Class `Component` with `__init__()/setup()` and `render()`
|
|
77
|
+
- `Props`, where every attribute is an accessor
|
|
78
|
+
- first-class component children through `props.children`
|
|
79
|
+
- transparent `Fragment(...)` nodes for returning multiple children
|
|
80
|
+
- widget namespaces: `tk` for classic Tk widgets and `ttk` for themed widgets
|
|
81
|
+
- layout namespace: `layout.VStack`, `layout.HStack`, `layout.Grid`, `layout.Item`, `layout.GridItem`
|
|
82
|
+
- StyleX-ish style objects agnostic to tk/ttk widgets with `style.define()`, `style.merge()`, and `style.component()`.
|
|
83
|
+
- some control flow: `Show`, `For`, `Switch` / `Match`, `Index`, `Dynamic`
|
|
84
|
+
- context: `create_context()`, `Provider`, `use_context()`
|
|
85
|
+
- stores: `create_store()` for immutable updates and `create_mutable()` for
|
|
86
|
+
Solid-style mutable object state
|
|
87
|
+
- resources: `create_resource()` with `loading`, `error`, `state`, `mutate`, `refetch`
|
|
88
|
+
- lifecycle helpers: `create_effect()`, `on_mount()`, `on_cleanup()`
|
|
89
|
+
- `create_root()` and explicit disposal through the returned `Mount`
|
|
90
|
+
|
|
91
|
+
## Doc Topics
|
|
92
|
+
|
|
93
|
+
- [Context](docs/context.md): providers, typed context helpers, and forwarded children.
|
|
94
|
+
- [Control](docs/control.md): conditional rendering.
|
|
95
|
+
- [Reactive primitives](docs/reactive.md): signals, memos, effects, and owner cleanup.
|
|
96
|
+
- [Resources](docs/resources.md): worker-thread loading, refreshes, mutation, and status.
|
|
97
|
+
- [Scheduling](docs/scheduling.md): queueing work
|
|
98
|
+
- [Stores](docs/stores.md): state management
|
|
99
|
+
- [Style](docs/style.md): StyleX-ish style objects, merging, and component styling.
|
|
100
|
+
- [Widgets](docs/widgets.md): Tkinter primitives
|
|
101
|
+
|
|
102
|
+
## Examples
|
|
103
|
+
|
|
104
|
+
The runnable examples live in [examples](../examples/). Start with the
|
|
105
|
+
[examples README](../examples/README.md) for a short path through them.
|
|
106
|
+
|
|
107
|
+
## Strong Typing
|
|
108
|
+
To follow Solid's model of named args collected into reactive props, there is some brittle trickery.
|
|
109
|
+
|
|
110
|
+
In JS/TS land, there's tons of tooling around jsx/tsx. Python doesn't have that.<br>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.
|
|
111
|
+
|
|
112
|
+
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.
|
|
113
|
+
|
|
114
|
+
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.
|
|
115
|
+
|
|
116
|
+
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).
|
|
117
|
+
|
|
118
|
+
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.
|
|
119
|
+
|
|
120
|
+
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.
|
|
121
|
+
|
|
122
|
+
## Design Notes
|
|
123
|
+
|
|
124
|
+
`create_signal()` returns an accessor and a mutator:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from taut.reactive import create_signal
|
|
128
|
+
count, set_count = create_signal(0)
|
|
129
|
+
count()
|
|
130
|
+
set_count(lambda value: value + 1)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The accessor is still compatible with `reaktiv` signals, so widgets can bind to
|
|
134
|
+
it directly. Writable widgets such as `tk.Entry(value=..., on_input=...)` receive
|
|
135
|
+
the accessor and mutator separately.
|
|
136
|
+
|
|
137
|
+
`Component.__new__` returns a renderable node to keep the class API simple
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
Counter(title="Solid TK")
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Inside a component, `self.props.title()` reads a reaktiv signal. This is
|
|
144
|
+
intentionally accessor-oriented.
|
|
145
|
+
|
|
146
|
+
Existing signals are preserved, while plain values and callbacks are wrapped as
|
|
147
|
+
signal values. Tk widget props use one additional convention: callable non-event
|
|
148
|
+
props are treated as reactive bindings, and event props such as `on_click` /
|
|
149
|
+
`command` are passed through as callbacks.
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
tk.Label(text=f"{count()}") # snapshot now
|
|
153
|
+
tk.Label(text=count) # reactive signal value
|
|
154
|
+
tk.Label(text=lambda: f"{count()}") # reactive derived expression
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
That means a component prop can be read inside a derived expression:
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
tk.Label(text=lambda: f"Hello {self.props.name()}")
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
or forwarded directly to a widget prop:
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
tk.Label(text=self.props.name)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Component children are also collected into `props.children`, so container-like
|
|
170
|
+
components can feel like Solid components instead of manually passing a
|
|
171
|
+
`children=` prop:
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
@component
|
|
175
|
+
def panel(props):
|
|
176
|
+
return layout.VStack(
|
|
177
|
+
tk.Label(text=props.title),
|
|
178
|
+
props.children(),
|
|
179
|
+
padding=8,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
panel(tk.Label(text="Nested"), title="Details")
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Control-flow nodes and `Fragment(...)` are transparent to layout. They produce
|
|
186
|
+
children; the parent widget or layout helper decides where those children go:
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
@component
|
|
190
|
+
def rows(props):
|
|
191
|
+
return For(props.items, lambda item: tk.Label(text=item), key=lambda item: item)
|
|
192
|
+
|
|
193
|
+
layout.VStack(
|
|
194
|
+
tk.Label(text="Before"),
|
|
195
|
+
rows(items=todos),
|
|
196
|
+
tk.Label(text="After"),
|
|
197
|
+
)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
In that example, the repeated labels are laid out by `VStack` between
|
|
201
|
+
`Before` and `After`; `For` and the component do not create wrapper frames.
|
|
202
|
+
|
|
203
|
+
## Benchmarks
|
|
204
|
+
|
|
205
|
+
To estimate framework overhead without needing a display server, run the fake-Tk
|
|
206
|
+
benchmark:
|
|
207
|
+
|
|
208
|
+
```sh
|
|
209
|
+
uv run python benchmarks/bench_overhead.py
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
It compares raw Tk-style widget creation with taut.tk mounting for static
|
|
213
|
+
labels, reactive label props, and a component wrapper. The most useful line is
|
|
214
|
+
the reported extra microseconds per widget, because native Tk/Tcl startup and
|
|
215
|
+
platform display behavior are intentionally excluded.
|
taut_tk-0.1.0/README.md
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# taut.tk
|
|
2
|
+
|
|
3
|
+
A small SolidJS-inspired runtime for Tkinter using
|
|
4
|
+
[reaktiv](https://pypi.org/project/reaktiv/) for fine-grained signals.
|
|
5
|
+
|
|
6
|
+
This is currently a prototype that I hope makes Tkinter more fun
|
|
7
|
+
|
|
8
|
+
```python
|
|
9
|
+
from typing import Protocol
|
|
10
|
+
|
|
11
|
+
from taut import component
|
|
12
|
+
from taut import layout
|
|
13
|
+
from taut import tk
|
|
14
|
+
from taut.reactive import Accessor
|
|
15
|
+
from taut.reactive import Mutator
|
|
16
|
+
from taut.reactive import create_signal
|
|
17
|
+
from taut.runtime import create_root
|
|
18
|
+
from taut.control import For
|
|
19
|
+
from taut.control import Show
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CounterProps(Protocol):
|
|
23
|
+
label: Accessor[str]
|
|
24
|
+
count: Accessor[int]
|
|
25
|
+
set_count: Mutator[int]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@component
|
|
29
|
+
def counter(props: CounterProps):
|
|
30
|
+
todos, set_todos = create_signal(["wire props", "own effects", "dispose cleanly"])
|
|
31
|
+
|
|
32
|
+
return layout.VStack(
|
|
33
|
+
tk.Label(text=lambda: f"{props.label()}: {props.count()}"),
|
|
34
|
+
tk.Button(text="Increment", on_click=lambda: props.set_count(lambda n: n + 1)),
|
|
35
|
+
Show(
|
|
36
|
+
lambda: props.count() % 2 == 0,
|
|
37
|
+
lambda: tk.Label(text="Even"),
|
|
38
|
+
fallback=lambda: tk.Label(text="Odd"),
|
|
39
|
+
),
|
|
40
|
+
For(todos, lambda item: tk.Label(text=item), key=lambda item: item),
|
|
41
|
+
layout.HStack(
|
|
42
|
+
tk.Button(text="-", on_click=lambda: set_todos(lambda items: items[:-1])),
|
|
43
|
+
gap=6,
|
|
44
|
+
),
|
|
45
|
+
padding=12,
|
|
46
|
+
gap=6,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
count, set_count = create_signal(0)
|
|
50
|
+
mount = create_root(
|
|
51
|
+
lambda: counter(label="Solid TK", count=count, set_count=set_count),
|
|
52
|
+
title="Solid TK",
|
|
53
|
+
)
|
|
54
|
+
mount.widget.mainloop()
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Why Tk?
|
|
58
|
+
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.
|
|
59
|
+
|
|
60
|
+
## What's Here So Far
|
|
61
|
+
|
|
62
|
+
- Functional components using `@component` decorator
|
|
63
|
+
- Class `Component` with `__init__()/setup()` and `render()`
|
|
64
|
+
- `Props`, where every attribute is an accessor
|
|
65
|
+
- first-class component children through `props.children`
|
|
66
|
+
- transparent `Fragment(...)` nodes for returning multiple children
|
|
67
|
+
- widget namespaces: `tk` for classic Tk widgets and `ttk` for themed widgets
|
|
68
|
+
- layout namespace: `layout.VStack`, `layout.HStack`, `layout.Grid`, `layout.Item`, `layout.GridItem`
|
|
69
|
+
- StyleX-ish style objects agnostic to tk/ttk widgets with `style.define()`, `style.merge()`, and `style.component()`.
|
|
70
|
+
- some control flow: `Show`, `For`, `Switch` / `Match`, `Index`, `Dynamic`
|
|
71
|
+
- context: `create_context()`, `Provider`, `use_context()`
|
|
72
|
+
- stores: `create_store()` for immutable updates and `create_mutable()` for
|
|
73
|
+
Solid-style mutable object state
|
|
74
|
+
- resources: `create_resource()` with `loading`, `error`, `state`, `mutate`, `refetch`
|
|
75
|
+
- lifecycle helpers: `create_effect()`, `on_mount()`, `on_cleanup()`
|
|
76
|
+
- `create_root()` and explicit disposal through the returned `Mount`
|
|
77
|
+
|
|
78
|
+
## Doc Topics
|
|
79
|
+
|
|
80
|
+
- [Context](docs/context.md): providers, typed context helpers, and forwarded children.
|
|
81
|
+
- [Control](docs/control.md): conditional rendering.
|
|
82
|
+
- [Reactive primitives](docs/reactive.md): signals, memos, effects, and owner cleanup.
|
|
83
|
+
- [Resources](docs/resources.md): worker-thread loading, refreshes, mutation, and status.
|
|
84
|
+
- [Scheduling](docs/scheduling.md): queueing work
|
|
85
|
+
- [Stores](docs/stores.md): state management
|
|
86
|
+
- [Style](docs/style.md): StyleX-ish style objects, merging, and component styling.
|
|
87
|
+
- [Widgets](docs/widgets.md): Tkinter primitives
|
|
88
|
+
|
|
89
|
+
## Examples
|
|
90
|
+
|
|
91
|
+
The runnable examples live in [examples](../examples/). Start with the
|
|
92
|
+
[examples README](../examples/README.md) for a short path through them.
|
|
93
|
+
|
|
94
|
+
## Strong Typing
|
|
95
|
+
To follow Solid's model of named args collected into reactive props, there is some brittle trickery.
|
|
96
|
+
|
|
97
|
+
In JS/TS land, there's tons of tooling around jsx/tsx. Python doesn't have that.<br>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.
|
|
98
|
+
|
|
99
|
+
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.
|
|
100
|
+
|
|
101
|
+
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.
|
|
102
|
+
|
|
103
|
+
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).
|
|
104
|
+
|
|
105
|
+
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.
|
|
106
|
+
|
|
107
|
+
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.
|
|
108
|
+
|
|
109
|
+
## Design Notes
|
|
110
|
+
|
|
111
|
+
`create_signal()` returns an accessor and a mutator:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from taut.reactive import create_signal
|
|
115
|
+
count, set_count = create_signal(0)
|
|
116
|
+
count()
|
|
117
|
+
set_count(lambda value: value + 1)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The accessor is still compatible with `reaktiv` signals, so widgets can bind to
|
|
121
|
+
it directly. Writable widgets such as `tk.Entry(value=..., on_input=...)` receive
|
|
122
|
+
the accessor and mutator separately.
|
|
123
|
+
|
|
124
|
+
`Component.__new__` returns a renderable node to keep the class API simple
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
Counter(title="Solid TK")
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Inside a component, `self.props.title()` reads a reaktiv signal. This is
|
|
131
|
+
intentionally accessor-oriented.
|
|
132
|
+
|
|
133
|
+
Existing signals are preserved, while plain values and callbacks are wrapped as
|
|
134
|
+
signal values. Tk widget props use one additional convention: callable non-event
|
|
135
|
+
props are treated as reactive bindings, and event props such as `on_click` /
|
|
136
|
+
`command` are passed through as callbacks.
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
tk.Label(text=f"{count()}") # snapshot now
|
|
140
|
+
tk.Label(text=count) # reactive signal value
|
|
141
|
+
tk.Label(text=lambda: f"{count()}") # reactive derived expression
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
That means a component prop can be read inside a derived expression:
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
tk.Label(text=lambda: f"Hello {self.props.name()}")
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
or forwarded directly to a widget prop:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
tk.Label(text=self.props.name)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Component children are also collected into `props.children`, so container-like
|
|
157
|
+
components can feel like Solid components instead of manually passing a
|
|
158
|
+
`children=` prop:
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
@component
|
|
162
|
+
def panel(props):
|
|
163
|
+
return layout.VStack(
|
|
164
|
+
tk.Label(text=props.title),
|
|
165
|
+
props.children(),
|
|
166
|
+
padding=8,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
panel(tk.Label(text="Nested"), title="Details")
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Control-flow nodes and `Fragment(...)` are transparent to layout. They produce
|
|
173
|
+
children; the parent widget or layout helper decides where those children go:
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
@component
|
|
177
|
+
def rows(props):
|
|
178
|
+
return For(props.items, lambda item: tk.Label(text=item), key=lambda item: item)
|
|
179
|
+
|
|
180
|
+
layout.VStack(
|
|
181
|
+
tk.Label(text="Before"),
|
|
182
|
+
rows(items=todos),
|
|
183
|
+
tk.Label(text="After"),
|
|
184
|
+
)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
In that example, the repeated labels are laid out by `VStack` between
|
|
188
|
+
`Before` and `After`; `For` and the component do not create wrapper frames.
|
|
189
|
+
|
|
190
|
+
## Benchmarks
|
|
191
|
+
|
|
192
|
+
To estimate framework overhead without needing a display server, run the fake-Tk
|
|
193
|
+
benchmark:
|
|
194
|
+
|
|
195
|
+
```sh
|
|
196
|
+
uv run python benchmarks/bench_overhead.py
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
It compares raw Tk-style widget creation with taut.tk mounting for static
|
|
200
|
+
labels, reactive label props, and a component wrapper. The most useful line is
|
|
201
|
+
the reported extra microseconds per widget, because native Tk/Tcl startup and
|
|
202
|
+
platform display behavior are intentionally excluded.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "taut.tk"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Add your description here"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Sam I", email = "aweptimum@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.13"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"reaktiv>=0.22.0",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[project.scripts]
|
|
15
|
+
taut = "taut.cli:main"
|
|
16
|
+
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
dev = [
|
|
19
|
+
"pillow>=12.2.0",
|
|
20
|
+
"watchdog>=6.0.0",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[build-system]
|
|
24
|
+
requires = ["uv_build>=0.11.21,<0.12.0"]
|
|
25
|
+
build-backend = "uv_build"
|
|
26
|
+
|
|
27
|
+
[tool.uv.build-backend]
|
|
28
|
+
module-name = "taut"
|
|
29
|
+
|
|
30
|
+
[dependency-groups]
|
|
31
|
+
dev = [
|
|
32
|
+
"pyright>=1.1.410",
|
|
33
|
+
"pytest>=9.1.1",
|
|
34
|
+
"ruff>=0.15.18",
|
|
35
|
+
]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""A Solid-style layer for Tkinter powered by reaktiv."""
|
|
2
|
+
|
|
3
|
+
from . import context
|
|
4
|
+
from . import control
|
|
5
|
+
from . import layout
|
|
6
|
+
from . import props
|
|
7
|
+
from . import reactive
|
|
8
|
+
from . import resource
|
|
9
|
+
from . import runtime
|
|
10
|
+
from . import stores
|
|
11
|
+
from . import style
|
|
12
|
+
from . import tk
|
|
13
|
+
from . import tk_props
|
|
14
|
+
from . import ttk
|
|
15
|
+
from .component import Component
|
|
16
|
+
from .component import component
|
|
17
|
+
from .runtime import Fragment
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"Component",
|
|
21
|
+
"Fragment",
|
|
22
|
+
"component",
|
|
23
|
+
"context",
|
|
24
|
+
"control",
|
|
25
|
+
"layout",
|
|
26
|
+
"props",
|
|
27
|
+
"reactive",
|
|
28
|
+
"resource",
|
|
29
|
+
"runtime",
|
|
30
|
+
"style",
|
|
31
|
+
"stores",
|
|
32
|
+
"tk",
|
|
33
|
+
"tk_props",
|
|
34
|
+
"ttk",
|
|
35
|
+
]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from taut.cli.stubs import main_cli as stubby
|
|
5
|
+
from taut.cli.watch import main as watcher
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main():
|
|
9
|
+
parser = argparse.ArgumentParser(description="taut.tk development tools.")
|
|
10
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
11
|
+
|
|
12
|
+
parser_stubs = subparsers.add_parser("stubs", help="Generate taut.tk .pyi stubs.")
|
|
13
|
+
parser_stubs.add_argument("paths", nargs="+", type=Path)
|
|
14
|
+
parser_stubs.add_argument(
|
|
15
|
+
"-o",
|
|
16
|
+
"--out-dir",
|
|
17
|
+
default="typings",
|
|
18
|
+
type=Path,
|
|
19
|
+
help="Directory for generated stubs. Defaults to typings.",
|
|
20
|
+
)
|
|
21
|
+
parser_stubs.add_argument(
|
|
22
|
+
"--in-place",
|
|
23
|
+
action="store_true",
|
|
24
|
+
help="Write sibling .pyi files next to source files.",
|
|
25
|
+
)
|
|
26
|
+
parser_stubs.set_defaults(func=stubby)
|
|
27
|
+
|
|
28
|
+
parser_watch = subparsers.add_parser("watch", help="Regenerate .pyi stubs on edits.")
|
|
29
|
+
parser_watch.set_defaults(func=watcher)
|
|
30
|
+
|
|
31
|
+
args = parser.parse_args()
|
|
32
|
+
args.func(args)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
if __name__ == "__main__":
|
|
36
|
+
main()
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class PropField:
|
|
9
|
+
name: str
|
|
10
|
+
internal_type: str
|
|
11
|
+
external_type: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class ComponentStub:
|
|
16
|
+
name: str
|
|
17
|
+
fields: list[PropField]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class ImportStub:
|
|
22
|
+
module: str
|
|
23
|
+
name: str
|
|
24
|
+
export_name: str
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def collect_protocols(module: ast.Module) -> dict[str, list[PropField]]:
|
|
28
|
+
protocols: dict[str, list[PropField]] = {}
|
|
29
|
+
for node in module.body:
|
|
30
|
+
if not isinstance(node, ast.ClassDef):
|
|
31
|
+
continue
|
|
32
|
+
if not any(base_name(base) == "Protocol" for base in node.bases):
|
|
33
|
+
continue
|
|
34
|
+
|
|
35
|
+
protocols[node.name] = [
|
|
36
|
+
PropField(
|
|
37
|
+
statement.target.id,
|
|
38
|
+
ast.unparse(statement.annotation),
|
|
39
|
+
external_prop_type(ast.unparse(statement.annotation)),
|
|
40
|
+
)
|
|
41
|
+
for statement in node.body
|
|
42
|
+
if isinstance(statement, ast.AnnAssign)
|
|
43
|
+
and isinstance(statement.target, ast.Name)
|
|
44
|
+
]
|
|
45
|
+
return protocols
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def collect_components(
|
|
49
|
+
module: ast.Module,
|
|
50
|
+
protocols: dict[str, list[PropField]],
|
|
51
|
+
) -> list[ComponentStub]:
|
|
52
|
+
components: list[ComponentStub] = []
|
|
53
|
+
for node in module.body:
|
|
54
|
+
if isinstance(node, ast.FunctionDef) and is_component_function(node):
|
|
55
|
+
protocol_name = first_arg_annotation(node)
|
|
56
|
+
elif isinstance(node, ast.ClassDef) and is_component_class(node):
|
|
57
|
+
protocol_name = init_props_annotation(node)
|
|
58
|
+
else:
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
if protocol_name is None:
|
|
62
|
+
components.append(ComponentStub(node.name, []))
|
|
63
|
+
elif protocol_name in protocols:
|
|
64
|
+
components.append(ComponentStub(node.name, protocols[protocol_name]))
|
|
65
|
+
return components
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def collect_public_imports(module: ast.Module) -> list[ImportStub]:
|
|
69
|
+
imports: list[ImportStub] = []
|
|
70
|
+
for node in module.body:
|
|
71
|
+
if not isinstance(node, ast.ImportFrom) or node.module is None:
|
|
72
|
+
continue
|
|
73
|
+
if node.module in {"__future__", "typing"} or node.module.startswith("solid_tk"):
|
|
74
|
+
continue
|
|
75
|
+
module_name = "." * node.level + node.module
|
|
76
|
+
for alias in node.names:
|
|
77
|
+
export_name = alias.asname or alias.name
|
|
78
|
+
if export_name.startswith("_"):
|
|
79
|
+
continue
|
|
80
|
+
imports.append(ImportStub(module_name, alias.name, export_name))
|
|
81
|
+
return imports
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def render_component(component: ComponentStub) -> list[str]:
|
|
85
|
+
fields = [field for field in component.fields if field.name != "children"]
|
|
86
|
+
children_field = next(
|
|
87
|
+
(field for field in component.fields if field.name == "children"),
|
|
88
|
+
None,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
if not component.fields:
|
|
92
|
+
return [
|
|
93
|
+
f"def {component.name}("
|
|
94
|
+
"*child_nodes: Any, children: Any = ..."
|
|
95
|
+
") -> runtime.Node: ..."
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
lines = [f"def {component.name}(", " *child_nodes: Any,"]
|
|
99
|
+
lines.extend(f" {field.name}: {field.external_type}," for field in fields)
|
|
100
|
+
if children_field is not None:
|
|
101
|
+
lines.append(f" children: {children_field.external_type},")
|
|
102
|
+
else:
|
|
103
|
+
lines.append(" children: Any = ...,")
|
|
104
|
+
lines.append(") -> runtime.Node: ...")
|
|
105
|
+
return lines
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def is_component_function(node: ast.FunctionDef) -> bool:
|
|
109
|
+
return any(base_name(decorator) == "component" for decorator in node.decorator_list)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def is_component_class(node: ast.ClassDef) -> bool:
|
|
113
|
+
return any(base_name(base) == "Component" for base in node.bases)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def first_arg_annotation(node: ast.FunctionDef) -> str | None:
|
|
117
|
+
if not node.args.args:
|
|
118
|
+
return None
|
|
119
|
+
annotation = node.args.args[0].annotation
|
|
120
|
+
return ast.unparse(annotation) if annotation is not None else None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def init_props_annotation(node: ast.ClassDef) -> str | None:
|
|
124
|
+
for statement in node.body:
|
|
125
|
+
if not isinstance(statement, ast.FunctionDef) or statement.name != "__init__":
|
|
126
|
+
continue
|
|
127
|
+
if len(statement.args.args) < 2:
|
|
128
|
+
return None
|
|
129
|
+
annotation = statement.args.args[1].annotation
|
|
130
|
+
return ast.unparse(annotation) if annotation is not None else None
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def external_prop_type(internal_type: str) -> str:
|
|
135
|
+
if internal_type.startswith("Accessor[") and internal_type.endswith("]"):
|
|
136
|
+
inner = internal_type.removeprefix("Accessor[").removesuffix("]")
|
|
137
|
+
return f"{inner} | reactive.Accessor[{inner}]"
|
|
138
|
+
if internal_type.startswith("reactive.Accessor[") and internal_type.endswith("]"):
|
|
139
|
+
inner = internal_type.removeprefix("reactive.Accessor[").removesuffix("]")
|
|
140
|
+
return f"{inner} | reactive.Accessor[{inner}]"
|
|
141
|
+
if internal_type.startswith("Mutator[") and internal_type.endswith("]"):
|
|
142
|
+
inner = internal_type.removeprefix("Mutator[").removesuffix("]")
|
|
143
|
+
return f"reactive.Mutator[{inner}]"
|
|
144
|
+
if internal_type.startswith("reactive.Mutator[") and internal_type.endswith("]"):
|
|
145
|
+
return internal_type
|
|
146
|
+
return "Any"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def base_name(node: ast.AST) -> str:
|
|
150
|
+
match node:
|
|
151
|
+
case ast.Name(id=name):
|
|
152
|
+
return name
|
|
153
|
+
case ast.Attribute(attr=name):
|
|
154
|
+
return name
|
|
155
|
+
case ast.Call(func=func):
|
|
156
|
+
return base_name(func)
|
|
157
|
+
case ast.Subscript(value=value):
|
|
158
|
+
return base_name(value)
|
|
159
|
+
case _:
|
|
160
|
+
return ""
|
|
161
|
+
|