nextpytk 0.2.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.
- nextpytk-0.2.0/.gitignore +4 -0
- nextpytk-0.2.0/LICENSE +21 -0
- nextpytk-0.2.0/PKG-INFO +301 -0
- nextpytk-0.2.0/README.md +290 -0
- nextpytk-0.2.0/ROADMAP.md +68 -0
- nextpytk-0.2.0/pyproject.toml +30 -0
- nextpytk-0.2.0/src/nextpytk/__init__.py +8 -0
- nextpytk-0.2.0/src/nextpytk/app.py +1376 -0
- nextpytk-0.2.0/src/nextpytk/layout.py +512 -0
- nextpytk-0.2.0/src/nextpytk/types.py +160 -0
- nextpytk-0.2.0/src/nextpytk/widgets.py +45 -0
nextpytk-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Takuya Nishimoto
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
nextpytk-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nextpytk
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Flask-style decorator API for tkinter GUI: schema-driven, A11y-first, agent-compatible.
|
|
5
|
+
Project-URL: Homepage, https://github.com/nishimotz/tk-outer
|
|
6
|
+
Author: Takuya Nishimoto
|
|
7
|
+
License: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Python: >=3.13
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# nextpytk — Flask-style Decorator API for Tkinter
|
|
13
|
+
|
|
14
|
+
nextpytk wraps Tkinter in Python decorators, inspired by Flask.
|
|
15
|
+
Widget registration and layout are decoupled via dependency injection.
|
|
16
|
+
All widgets expose a JSON schema for AI/LLM consumption.
|
|
17
|
+
Uses ttk widgets where available (Button, Entry, Checkbutton, Radiobutton, Scale, Spinbox, Notebook).
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from nextpytk import TkApp, Layout
|
|
25
|
+
|
|
26
|
+
app = TkApp(title="Hello")
|
|
27
|
+
|
|
28
|
+
@app.status("msg")
|
|
29
|
+
def msg():
|
|
30
|
+
return "Hello, world!"
|
|
31
|
+
|
|
32
|
+
@app.button("greet", label="Greet")
|
|
33
|
+
def on_greet(values):
|
|
34
|
+
return {"msg": "Button clicked!"}
|
|
35
|
+
|
|
36
|
+
app.run(layout=Layout().section("msg").section("greet"))
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Three layout styles — pick the one that fits:**
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
# 1) Simple list (easiest)
|
|
43
|
+
app.run(layout=["msg", "greet"])
|
|
44
|
+
|
|
45
|
+
# 2) Fluent DSL
|
|
46
|
+
app.run(layout=Layout().section("msg").section("greet"))
|
|
47
|
+
|
|
48
|
+
# 3) with-block (context manager)
|
|
49
|
+
with app.layout() as b:
|
|
50
|
+
b.section("msg")
|
|
51
|
+
b.section("greet")
|
|
52
|
+
app.run(layout=b.build())
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Multiview (Multi-tab)
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from nextpytk import TkApp, Layout
|
|
61
|
+
|
|
62
|
+
app = TkApp(title="Multi-tab App")
|
|
63
|
+
|
|
64
|
+
@app.status("header")
|
|
65
|
+
def header(): return "Common header"
|
|
66
|
+
|
|
67
|
+
with app.view("Tab1", layout=Layout().section("t1_label", "t1_btn")) as v:
|
|
68
|
+
@v.label("t1_label")
|
|
69
|
+
def t1_label(): return "Tab 1 content"
|
|
70
|
+
@v.button("t1_btn", label="Click")
|
|
71
|
+
def t1_btn(vals): return {}
|
|
72
|
+
|
|
73
|
+
with app.view("Tab2", layout=Layout().section("t2_label")) as v:
|
|
74
|
+
@v.label("t2_label")
|
|
75
|
+
def t2_label(): return "Tab 2 content"
|
|
76
|
+
|
|
77
|
+
@app.multiview(
|
|
78
|
+
"main",
|
|
79
|
+
views=["Tab1", "Tab2"],
|
|
80
|
+
toplevel_widgets=("header",),
|
|
81
|
+
initial_state={"tab": "Tab1"},
|
|
82
|
+
on_tab_change=lambda tab: {"tab": tab},
|
|
83
|
+
)
|
|
84
|
+
def main_multiview(): pass
|
|
85
|
+
|
|
86
|
+
app.run(multiview="main")
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
View layouts also accept lists or with-block builders:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
@app.multiview("main", views=["Home", "Settings"],
|
|
93
|
+
view_layouts={"Home": ["title", "start"], "Settings": ["timer", "status"]})
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Layout DSL
|
|
99
|
+
|
|
100
|
+
### Simple list
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
app.run(layout=["title", "timer", "start", "status"])
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Each name gets its own pack-based section. Extra kwargs forwarded to `section()`:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
Layout.from_list(["a", "b"], fill="both", expand=True)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Fluent DSL
|
|
113
|
+
|
|
114
|
+
**Pack sections:**
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
Layout().section("msg").section("phase", "count").section("start", "pause")
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**Grid builder:**
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from nextpytk.types import Sticky
|
|
124
|
+
|
|
125
|
+
Layout().grid()
|
|
126
|
+
.span(2).widget("title", sticky=Sticky.W)
|
|
127
|
+
.next_row()
|
|
128
|
+
.widget("label", sticky=Sticky.RIGHT).widget("input", sticky=Sticky.LEFT_RIGHT)
|
|
129
|
+
.next_row()
|
|
130
|
+
.span(2).widget("ok")
|
|
131
|
+
.end_grid()
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Grid builder methods:
|
|
135
|
+
|
|
136
|
+
| Method | Description |
|
|
137
|
+
|--------|-------------|
|
|
138
|
+
| `widget(name, *, sticky, padx, pady, colspan, rowspan)` | Place widget at cursor, advance column |
|
|
139
|
+
| `span(n)` | Set colspan for the next `widget()` call |
|
|
140
|
+
| `next_row()` | Move to next row, reset column |
|
|
141
|
+
| `next_col(n)` | Skip n columns |
|
|
142
|
+
| `at(row, col)` | Jump to absolute position |
|
|
143
|
+
| `col_weights(*w)` | Bulk column weights: `col_weights(0, 1, 1)` |
|
|
144
|
+
| `row_weights(*w)` | Bulk row weights |
|
|
145
|
+
| `col_weight(col, w)` | Single column weight |
|
|
146
|
+
| `row_weight(row, w)` | Single row weight |
|
|
147
|
+
| `col_minsize(col, px)` | Column minimum width |
|
|
148
|
+
| `row_minsize(row, px)` | Row minimum height |
|
|
149
|
+
| `end_grid()` | Return to Layout chain |
|
|
150
|
+
|
|
151
|
+
`col_weights(0, 1, 1)` means column 0 → weight 0, column 1 → weight 1, column 2 → weight 1.
|
|
152
|
+
|
|
153
|
+
### With-block (context manager)
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
from nextpytk import LayoutBuilder
|
|
157
|
+
|
|
158
|
+
# Standalone builder
|
|
159
|
+
builder = LayoutBuilder()
|
|
160
|
+
with builder:
|
|
161
|
+
builder.section("title")
|
|
162
|
+
with builder.grid(col_weights=(0, 1)):
|
|
163
|
+
builder.widget("celsius", sticky="ew")
|
|
164
|
+
builder.widget("fahrenheit", sticky="ew")
|
|
165
|
+
builder.next_row().span(2).widget("note")
|
|
166
|
+
app.run(layout=builder.build())
|
|
167
|
+
|
|
168
|
+
# Via app.layout() shortcut
|
|
169
|
+
with app.layout() as b:
|
|
170
|
+
b.section("title")
|
|
171
|
+
with b.grid(col_weights=(0, 1)):
|
|
172
|
+
b.widget("celsius", sticky="ew")
|
|
173
|
+
app.run(layout=b.build())
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`with b.grid(...)` auto-closes — no `end_grid()` needed.
|
|
177
|
+
|
|
178
|
+
`grid()` options available directly: `col_weights=(0,1)`, `row_weights=(...)`, `padx`, `pady`, `fill`, `expand`, `uniform`.
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Widget Reference
|
|
183
|
+
|
|
184
|
+
| Decorator | Widget | Callback receives | Returns |
|
|
185
|
+
|-----------|--------|-------------------|---------|
|
|
186
|
+
| `@app.label(name, font=..., anchor=..., justify=..., padding=...)` | tk.Label | — | `str` or `dict` |
|
|
187
|
+
| `@app.status(name)` | tk.Label (role=status) | — | `str` or `dict` |
|
|
188
|
+
| `@app.message(name, width=..., auto_width=...)` | tk.Label (auto-wrap) | — | `str` or `dict` |
|
|
189
|
+
| `@app.button(name, label=..., enabled_if=...)` | ttk.Button | entry values `dict` | `dict` |
|
|
190
|
+
| `@app.job(name)` | async callable | entry values `dict` | `dict` |
|
|
191
|
+
| `@app.entry(name, placeholder=..., show=...)` | ttk.Entry | `str` | `dict` |
|
|
192
|
+
| `@app.checkbutton(name, text=...)` | ttk.Checkbutton | `bool` | `dict` |
|
|
193
|
+
| `@app.radiobutton(name, text=..., value=..., group=...)` | ttk.Radiobutton | selected value `str` | `dict` |
|
|
194
|
+
| `@app.text(name, width=..., height=...)` | tk.Text | full content `str` | `dict` |
|
|
195
|
+
| `@app.scale(name, from_=..., to=..., orient=...)` | ttk.Scale | value `str` | `dict` |
|
|
196
|
+
| `@app.spinbox(name, from_=..., to=..., values=...)` | ttk.Spinbox | value `str` | `dict` |
|
|
197
|
+
| `@app.listbox(name, items=..., selectmode=...)` | tk.Listbox | selected item `str` | `dict` |
|
|
198
|
+
| `@app.canvas(name, width=..., height=...)` | tk.Canvas | — | — |
|
|
199
|
+
|
|
200
|
+
Label options:
|
|
201
|
+
- `font`: e.g. `font=("TkDefaultFont", 18, "bold")`
|
|
202
|
+
- `anchor`: e.g. `anchor="e"` (right-aligned)
|
|
203
|
+
- `justify`: multi-line alignment, e.g. `justify="right"`
|
|
204
|
+
- `padding`: e.g. `padding=4` or `padding=(4, 2)`
|
|
205
|
+
|
|
206
|
+
`@app.message` creates an auto-wrapping label. `width` sets initial pixel width; `auto_width=True` (default) tracks parent container resize.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Typed Constants
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
from nextpytk.types import Side, Fill, Sticky, State, Orient
|
|
214
|
+
|
|
215
|
+
Layout().section("msg", side=Side.LEFT, fill=Fill.X)
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Values use `str` literals compatible with tkinter. `SideLike` / `FillLike` etc.
|
|
219
|
+
accept raw strings too.
|
|
220
|
+
|
|
221
|
+
| Type | Namespace | Example |
|
|
222
|
+
|------|-----------|---------|
|
|
223
|
+
| `Side` | `Side.TOP/BOTTOM/LEFT/RIGHT` | pack side |
|
|
224
|
+
| `Fill` | `Fill.X/Y/BOTH/NONE` | pack fill |
|
|
225
|
+
| `Sticky` | `Sticky.NSEW/LEFT_RIGHT/TOP/BOTTOM/LEFT/RIGHT` | grid sticky |
|
|
226
|
+
| `State` | `State.NORMAL/DISABLED/ACTIVE` | widget state |
|
|
227
|
+
| `Orient` | `Orient.HORIZONTAL/VERTICAL` | scale orientation |
|
|
228
|
+
| `Relief` | `Relief.FLAT/RAISED/SUNKEN/GROOVE/RIDGE/SOLID` | border style |
|
|
229
|
+
| `Justify` | `Justify.LEFT/RIGHT/CENTER` | text alignment |
|
|
230
|
+
| `SelectMode` | `SelectMode.SINGLE/BROWSE/MULTIPLE/EXTENDED` | listbox mode |
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Schema Export (Agent/LLM)
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
@label("temperature")
|
|
238
|
+
def t(): return "25°C"
|
|
239
|
+
|
|
240
|
+
app.schema()
|
|
241
|
+
# → {"title": "...", "widgets": [{"name": "temperature", "kind": "label", ...}]}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Output is JSON-compatible and can serve as LLM Function Calling definitions.
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Async-Native (asyncio + Tkinter)
|
|
249
|
+
|
|
250
|
+
`app.run_async()` runs the app on an asyncio event loop, cooperatively scheduled
|
|
251
|
+
with the Tk main loop via `root.tk.dooneevent(0)`.
|
|
252
|
+
`app.spawn(coro)` schedules async tasks during GUI runtime.
|
|
253
|
+
`@app.job(name)` registers async callables.
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
@app.job("scan")
|
|
257
|
+
async def scan(vals):
|
|
258
|
+
result = await asyncio.to_thread(some_blocking_call)
|
|
259
|
+
return {"status": "done"}
|
|
260
|
+
|
|
261
|
+
app.run_async(layout=Layout().section("status"))
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Examples
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
uv run python examples/grid_temp.py # temperature converter
|
|
268
|
+
uv run python examples/task_panel.py # multi-button panel
|
|
269
|
+
uv run python examples/multiscreen.py # order app with screens
|
|
270
|
+
uv run python examples/widget_gallery.py # all widget types
|
|
271
|
+
uv run python examples/disk_usage_flat_viewer.py # ncdu-style viewer (sync)
|
|
272
|
+
uv run python examples/disk_usage_flat_async.py # ncdu-style viewer (async)
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## Requirements
|
|
278
|
+
|
|
279
|
+
- Python 3.14 by default (`PYTHON=...` override supported)
|
|
280
|
+
- Tkinter support in your Python build
|
|
281
|
+
- No other dependencies
|
|
282
|
+
|
|
283
|
+
> Note: On some macOS environments, `uv` + `3.14+freethreaded` can fail at Tk startup with `Can't find a usable init.tcl`.
|
|
284
|
+
> You can switch runtimes per command, e.g. `make run PYTHON=3.13`, `make run PYTHON=3.14+freethreaded`, `make run PYTHON=3.15`.
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## Related Projects
|
|
289
|
+
|
|
290
|
+
- **`tkinter` (stdlib)**: nextpytk builds on top — adding Decorator / Schema / A11y layers.
|
|
291
|
+
- **`ttk`**: Native look and accessibility; nextpytk prefers ttk widgets where available.
|
|
292
|
+
- **`CustomTkinter`**: Modern look via Canvas rendering. nextpytk takes the opposite approach: use native widgets and embed A11y from the start.
|
|
293
|
+
- **`TkRouter`** (israel-dryer, author of ttkbootstrap): Declarative view routing with URL-style paths, animated transitions, and history stack. Complements nextpytk's `multiview` — routing vs widget composition.
|
|
294
|
+
|
|
295
|
+
## License
|
|
296
|
+
|
|
297
|
+
MIT
|
|
298
|
+
|
|
299
|
+
## Author
|
|
300
|
+
|
|
301
|
+
Takuya Nishimoto — Shuaruta Inc.
|
nextpytk-0.2.0/README.md
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
# nextpytk — Flask-style Decorator API for Tkinter
|
|
2
|
+
|
|
3
|
+
nextpytk wraps Tkinter in Python decorators, inspired by Flask.
|
|
4
|
+
Widget registration and layout are decoupled via dependency injection.
|
|
5
|
+
All widgets expose a JSON schema for AI/LLM consumption.
|
|
6
|
+
Uses ttk widgets where available (Button, Entry, Checkbutton, Radiobutton, Scale, Spinbox, Notebook).
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Quick Start
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
from nextpytk import TkApp, Layout
|
|
14
|
+
|
|
15
|
+
app = TkApp(title="Hello")
|
|
16
|
+
|
|
17
|
+
@app.status("msg")
|
|
18
|
+
def msg():
|
|
19
|
+
return "Hello, world!"
|
|
20
|
+
|
|
21
|
+
@app.button("greet", label="Greet")
|
|
22
|
+
def on_greet(values):
|
|
23
|
+
return {"msg": "Button clicked!"}
|
|
24
|
+
|
|
25
|
+
app.run(layout=Layout().section("msg").section("greet"))
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Three layout styles — pick the one that fits:**
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
# 1) Simple list (easiest)
|
|
32
|
+
app.run(layout=["msg", "greet"])
|
|
33
|
+
|
|
34
|
+
# 2) Fluent DSL
|
|
35
|
+
app.run(layout=Layout().section("msg").section("greet"))
|
|
36
|
+
|
|
37
|
+
# 3) with-block (context manager)
|
|
38
|
+
with app.layout() as b:
|
|
39
|
+
b.section("msg")
|
|
40
|
+
b.section("greet")
|
|
41
|
+
app.run(layout=b.build())
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Multiview (Multi-tab)
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from nextpytk import TkApp, Layout
|
|
50
|
+
|
|
51
|
+
app = TkApp(title="Multi-tab App")
|
|
52
|
+
|
|
53
|
+
@app.status("header")
|
|
54
|
+
def header(): return "Common header"
|
|
55
|
+
|
|
56
|
+
with app.view("Tab1", layout=Layout().section("t1_label", "t1_btn")) as v:
|
|
57
|
+
@v.label("t1_label")
|
|
58
|
+
def t1_label(): return "Tab 1 content"
|
|
59
|
+
@v.button("t1_btn", label="Click")
|
|
60
|
+
def t1_btn(vals): return {}
|
|
61
|
+
|
|
62
|
+
with app.view("Tab2", layout=Layout().section("t2_label")) as v:
|
|
63
|
+
@v.label("t2_label")
|
|
64
|
+
def t2_label(): return "Tab 2 content"
|
|
65
|
+
|
|
66
|
+
@app.multiview(
|
|
67
|
+
"main",
|
|
68
|
+
views=["Tab1", "Tab2"],
|
|
69
|
+
toplevel_widgets=("header",),
|
|
70
|
+
initial_state={"tab": "Tab1"},
|
|
71
|
+
on_tab_change=lambda tab: {"tab": tab},
|
|
72
|
+
)
|
|
73
|
+
def main_multiview(): pass
|
|
74
|
+
|
|
75
|
+
app.run(multiview="main")
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
View layouts also accept lists or with-block builders:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
@app.multiview("main", views=["Home", "Settings"],
|
|
82
|
+
view_layouts={"Home": ["title", "start"], "Settings": ["timer", "status"]})
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Layout DSL
|
|
88
|
+
|
|
89
|
+
### Simple list
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
app.run(layout=["title", "timer", "start", "status"])
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Each name gets its own pack-based section. Extra kwargs forwarded to `section()`:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
Layout.from_list(["a", "b"], fill="both", expand=True)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Fluent DSL
|
|
102
|
+
|
|
103
|
+
**Pack sections:**
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
Layout().section("msg").section("phase", "count").section("start", "pause")
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**Grid builder:**
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from nextpytk.types import Sticky
|
|
113
|
+
|
|
114
|
+
Layout().grid()
|
|
115
|
+
.span(2).widget("title", sticky=Sticky.W)
|
|
116
|
+
.next_row()
|
|
117
|
+
.widget("label", sticky=Sticky.RIGHT).widget("input", sticky=Sticky.LEFT_RIGHT)
|
|
118
|
+
.next_row()
|
|
119
|
+
.span(2).widget("ok")
|
|
120
|
+
.end_grid()
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Grid builder methods:
|
|
124
|
+
|
|
125
|
+
| Method | Description |
|
|
126
|
+
|--------|-------------|
|
|
127
|
+
| `widget(name, *, sticky, padx, pady, colspan, rowspan)` | Place widget at cursor, advance column |
|
|
128
|
+
| `span(n)` | Set colspan for the next `widget()` call |
|
|
129
|
+
| `next_row()` | Move to next row, reset column |
|
|
130
|
+
| `next_col(n)` | Skip n columns |
|
|
131
|
+
| `at(row, col)` | Jump to absolute position |
|
|
132
|
+
| `col_weights(*w)` | Bulk column weights: `col_weights(0, 1, 1)` |
|
|
133
|
+
| `row_weights(*w)` | Bulk row weights |
|
|
134
|
+
| `col_weight(col, w)` | Single column weight |
|
|
135
|
+
| `row_weight(row, w)` | Single row weight |
|
|
136
|
+
| `col_minsize(col, px)` | Column minimum width |
|
|
137
|
+
| `row_minsize(row, px)` | Row minimum height |
|
|
138
|
+
| `end_grid()` | Return to Layout chain |
|
|
139
|
+
|
|
140
|
+
`col_weights(0, 1, 1)` means column 0 → weight 0, column 1 → weight 1, column 2 → weight 1.
|
|
141
|
+
|
|
142
|
+
### With-block (context manager)
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
from nextpytk import LayoutBuilder
|
|
146
|
+
|
|
147
|
+
# Standalone builder
|
|
148
|
+
builder = LayoutBuilder()
|
|
149
|
+
with builder:
|
|
150
|
+
builder.section("title")
|
|
151
|
+
with builder.grid(col_weights=(0, 1)):
|
|
152
|
+
builder.widget("celsius", sticky="ew")
|
|
153
|
+
builder.widget("fahrenheit", sticky="ew")
|
|
154
|
+
builder.next_row().span(2).widget("note")
|
|
155
|
+
app.run(layout=builder.build())
|
|
156
|
+
|
|
157
|
+
# Via app.layout() shortcut
|
|
158
|
+
with app.layout() as b:
|
|
159
|
+
b.section("title")
|
|
160
|
+
with b.grid(col_weights=(0, 1)):
|
|
161
|
+
b.widget("celsius", sticky="ew")
|
|
162
|
+
app.run(layout=b.build())
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`with b.grid(...)` auto-closes — no `end_grid()` needed.
|
|
166
|
+
|
|
167
|
+
`grid()` options available directly: `col_weights=(0,1)`, `row_weights=(...)`, `padx`, `pady`, `fill`, `expand`, `uniform`.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Widget Reference
|
|
172
|
+
|
|
173
|
+
| Decorator | Widget | Callback receives | Returns |
|
|
174
|
+
|-----------|--------|-------------------|---------|
|
|
175
|
+
| `@app.label(name, font=..., anchor=..., justify=..., padding=...)` | tk.Label | — | `str` or `dict` |
|
|
176
|
+
| `@app.status(name)` | tk.Label (role=status) | — | `str` or `dict` |
|
|
177
|
+
| `@app.message(name, width=..., auto_width=...)` | tk.Label (auto-wrap) | — | `str` or `dict` |
|
|
178
|
+
| `@app.button(name, label=..., enabled_if=...)` | ttk.Button | entry values `dict` | `dict` |
|
|
179
|
+
| `@app.job(name)` | async callable | entry values `dict` | `dict` |
|
|
180
|
+
| `@app.entry(name, placeholder=..., show=...)` | ttk.Entry | `str` | `dict` |
|
|
181
|
+
| `@app.checkbutton(name, text=...)` | ttk.Checkbutton | `bool` | `dict` |
|
|
182
|
+
| `@app.radiobutton(name, text=..., value=..., group=...)` | ttk.Radiobutton | selected value `str` | `dict` |
|
|
183
|
+
| `@app.text(name, width=..., height=...)` | tk.Text | full content `str` | `dict` |
|
|
184
|
+
| `@app.scale(name, from_=..., to=..., orient=...)` | ttk.Scale | value `str` | `dict` |
|
|
185
|
+
| `@app.spinbox(name, from_=..., to=..., values=...)` | ttk.Spinbox | value `str` | `dict` |
|
|
186
|
+
| `@app.listbox(name, items=..., selectmode=...)` | tk.Listbox | selected item `str` | `dict` |
|
|
187
|
+
| `@app.canvas(name, width=..., height=...)` | tk.Canvas | — | — |
|
|
188
|
+
|
|
189
|
+
Label options:
|
|
190
|
+
- `font`: e.g. `font=("TkDefaultFont", 18, "bold")`
|
|
191
|
+
- `anchor`: e.g. `anchor="e"` (right-aligned)
|
|
192
|
+
- `justify`: multi-line alignment, e.g. `justify="right"`
|
|
193
|
+
- `padding`: e.g. `padding=4` or `padding=(4, 2)`
|
|
194
|
+
|
|
195
|
+
`@app.message` creates an auto-wrapping label. `width` sets initial pixel width; `auto_width=True` (default) tracks parent container resize.
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Typed Constants
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
from nextpytk.types import Side, Fill, Sticky, State, Orient
|
|
203
|
+
|
|
204
|
+
Layout().section("msg", side=Side.LEFT, fill=Fill.X)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Values use `str` literals compatible with tkinter. `SideLike` / `FillLike` etc.
|
|
208
|
+
accept raw strings too.
|
|
209
|
+
|
|
210
|
+
| Type | Namespace | Example |
|
|
211
|
+
|------|-----------|---------|
|
|
212
|
+
| `Side` | `Side.TOP/BOTTOM/LEFT/RIGHT` | pack side |
|
|
213
|
+
| `Fill` | `Fill.X/Y/BOTH/NONE` | pack fill |
|
|
214
|
+
| `Sticky` | `Sticky.NSEW/LEFT_RIGHT/TOP/BOTTOM/LEFT/RIGHT` | grid sticky |
|
|
215
|
+
| `State` | `State.NORMAL/DISABLED/ACTIVE` | widget state |
|
|
216
|
+
| `Orient` | `Orient.HORIZONTAL/VERTICAL` | scale orientation |
|
|
217
|
+
| `Relief` | `Relief.FLAT/RAISED/SUNKEN/GROOVE/RIDGE/SOLID` | border style |
|
|
218
|
+
| `Justify` | `Justify.LEFT/RIGHT/CENTER` | text alignment |
|
|
219
|
+
| `SelectMode` | `SelectMode.SINGLE/BROWSE/MULTIPLE/EXTENDED` | listbox mode |
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Schema Export (Agent/LLM)
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
@label("temperature")
|
|
227
|
+
def t(): return "25°C"
|
|
228
|
+
|
|
229
|
+
app.schema()
|
|
230
|
+
# → {"title": "...", "widgets": [{"name": "temperature", "kind": "label", ...}]}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Output is JSON-compatible and can serve as LLM Function Calling definitions.
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Async-Native (asyncio + Tkinter)
|
|
238
|
+
|
|
239
|
+
`app.run_async()` runs the app on an asyncio event loop, cooperatively scheduled
|
|
240
|
+
with the Tk main loop via `root.tk.dooneevent(0)`.
|
|
241
|
+
`app.spawn(coro)` schedules async tasks during GUI runtime.
|
|
242
|
+
`@app.job(name)` registers async callables.
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
@app.job("scan")
|
|
246
|
+
async def scan(vals):
|
|
247
|
+
result = await asyncio.to_thread(some_blocking_call)
|
|
248
|
+
return {"status": "done"}
|
|
249
|
+
|
|
250
|
+
app.run_async(layout=Layout().section("status"))
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Examples
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
uv run python examples/grid_temp.py # temperature converter
|
|
257
|
+
uv run python examples/task_panel.py # multi-button panel
|
|
258
|
+
uv run python examples/multiscreen.py # order app with screens
|
|
259
|
+
uv run python examples/widget_gallery.py # all widget types
|
|
260
|
+
uv run python examples/disk_usage_flat_viewer.py # ncdu-style viewer (sync)
|
|
261
|
+
uv run python examples/disk_usage_flat_async.py # ncdu-style viewer (async)
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## Requirements
|
|
267
|
+
|
|
268
|
+
- Python 3.14 by default (`PYTHON=...` override supported)
|
|
269
|
+
- Tkinter support in your Python build
|
|
270
|
+
- No other dependencies
|
|
271
|
+
|
|
272
|
+
> Note: On some macOS environments, `uv` + `3.14+freethreaded` can fail at Tk startup with `Can't find a usable init.tcl`.
|
|
273
|
+
> You can switch runtimes per command, e.g. `make run PYTHON=3.13`, `make run PYTHON=3.14+freethreaded`, `make run PYTHON=3.15`.
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## Related Projects
|
|
278
|
+
|
|
279
|
+
- **`tkinter` (stdlib)**: nextpytk builds on top — adding Decorator / Schema / A11y layers.
|
|
280
|
+
- **`ttk`**: Native look and accessibility; nextpytk prefers ttk widgets where available.
|
|
281
|
+
- **`CustomTkinter`**: Modern look via Canvas rendering. nextpytk takes the opposite approach: use native widgets and embed A11y from the start.
|
|
282
|
+
- **`TkRouter`** (israel-dryer, author of ttkbootstrap): Declarative view routing with URL-style paths, animated transitions, and history stack. Complements nextpytk's `multiview` — routing vs widget composition.
|
|
283
|
+
|
|
284
|
+
## License
|
|
285
|
+
|
|
286
|
+
MIT
|
|
287
|
+
|
|
288
|
+
## Author
|
|
289
|
+
|
|
290
|
+
Takuya Nishimoto — Shuaruta Inc.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# ROADMAP — nextpytk
|
|
2
|
+
|
|
3
|
+
nextpytk の開発ロードマップ。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 直近(v0.2.x)
|
|
8
|
+
|
|
9
|
+
### 型ヒント強化
|
|
10
|
+
|
|
11
|
+
- [ ] デコレータ引数の TypedDict / Protocol 化(Pyright/mypy 補完)
|
|
12
|
+
- [ ] tkinter 定数を活かした Literal 型(`tk.LEFT`, `tk.RIGHT`, `tk.NSEW` など)
|
|
13
|
+
- [ ] `_GridBuilder.columnconfigure` 相当の fluent API
|
|
14
|
+
|
|
15
|
+
### ウィジェット拡張
|
|
16
|
+
|
|
17
|
+
- [x] ttk widget 対応(ttk.Button, ttk.Entry, ttk.Notebook など)
|
|
18
|
+
- [x] `@app.multiview` デコレータ(マルチタブ)
|
|
19
|
+
- [ ] `bind` イベントの decorator 登録
|
|
20
|
+
|
|
21
|
+
### A11y 実適用
|
|
22
|
+
|
|
23
|
+
- [ ] Tk 9.1 `::tk::accessible::*` への role/name 結線
|
|
24
|
+
- [ ] `WidgetSpec.role` → 実際の accessibility 属性反映
|
|
25
|
+
|
|
26
|
+
### Layout DSL 充実
|
|
27
|
+
|
|
28
|
+
- [x] `grid` の `rowconfigure` / `columnconfigure` 相当(`col_weights`/`row_weights`/`rowspan`)
|
|
29
|
+
- [ ] ネストフレーム(`Layout` 内で `Layout` を入れ子に)
|
|
30
|
+
- [ ] `padx`/`pady` のデフォルト値一元設定
|
|
31
|
+
|
|
32
|
+
### 公開ランタイム API
|
|
33
|
+
|
|
34
|
+
- [x] `build_widgets()`, `widget()`, `widget_kind()`, `widget_specs()`
|
|
35
|
+
- [x] `apply_state()`, `sync()` — カスタムランナーから再利用可能
|
|
36
|
+
- [x] `app.run(multiview="...")` エントリポイント
|
|
37
|
+
|
|
38
|
+
### Agent / LLM 連携
|
|
39
|
+
|
|
40
|
+
- [ ] `schema()` を Function Calling 定義としての露出改善
|
|
41
|
+
- [ ] `@agent_tool` 統合(GUI 操作をエージェント語彙として扱う)
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 中長期
|
|
46
|
+
|
|
47
|
+
### ttk Style レイヤー
|
|
48
|
+
|
|
49
|
+
- [ ] `Layout.style("my_button", background=..., font=...)` 的なスタイル定義
|
|
50
|
+
- [ ] テーマ切替(`ttk.Style().theme_use(...)`)
|
|
51
|
+
|
|
52
|
+
### 非同期ジョブ統合
|
|
53
|
+
|
|
54
|
+
- [x] `app.run_async()` + `app.spawn()` — async event loop と Tk の共存
|
|
55
|
+
- [x] `app.spawn(asyncio.to_thread(...))` で非ブロッキングバックグラウンドジョブ
|
|
56
|
+
- [x] 実例同期版: `disk_usage_flat_viewer.py`
|
|
57
|
+
- [x] 実例非同期版: `disk_usage_flat_async.py`(`app.run_async()` + `app.spawn()`)
|
|
58
|
+
- [x] `@app.job(name)` 連携 — async コールバックの @app デコレータ登録
|
|
59
|
+
|
|
60
|
+
### 宣言的コンポーネント
|
|
61
|
+
|
|
62
|
+
- [ ] `Layout` に代わる `@app.component` デコレータ(React ライクな再利用)
|
|
63
|
+
- [ ] state の型定義とバリデーション(ただし Pydantic は使わない)
|
|
64
|
+
|
|
65
|
+
### テスト
|
|
66
|
+
|
|
67
|
+
- [ ] ヘッドレス実行テスト(`TKOUTER_HEADLESS=1`)
|
|
68
|
+
- [ ] WidgetSpec 単位のユニットテスト
|