nextpytk 0.4.3__tar.gz → 0.4.4__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.4.3 → nextpytk-0.4.4}/PKG-INFO +67 -2
- {nextpytk-0.4.3 → nextpytk-0.4.4}/README.md +66 -1
- {nextpytk-0.4.3 → nextpytk-0.4.4}/ROADMAP.md +74 -1
- {nextpytk-0.4.3 → nextpytk-0.4.4}/pyproject.toml +1 -1
- {nextpytk-0.4.3 → nextpytk-0.4.4}/src/nextpytk/app.py +409 -29
- {nextpytk-0.4.3 → nextpytk-0.4.4}/src/nextpytk/layout.py +282 -4
- {nextpytk-0.4.3 → nextpytk-0.4.4}/src/nextpytk/types.py +27 -0
- {nextpytk-0.4.3 → nextpytk-0.4.4}/.gitignore +0 -0
- {nextpytk-0.4.3 → nextpytk-0.4.4}/LICENSE +0 -0
- {nextpytk-0.4.3 → nextpytk-0.4.4}/src/nextpytk/__init__.py +0 -0
- {nextpytk-0.4.3 → nextpytk-0.4.4}/src/nextpytk/theme.py +0 -0
- {nextpytk-0.4.3 → nextpytk-0.4.4}/src/nextpytk/tokens.py +0 -0
- {nextpytk-0.4.3 → nextpytk-0.4.4}/src/nextpytk/widgets.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nextpytk
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.4
|
|
4
4
|
Summary: Accessible, declarative Tkinter applications from ordinary Python functions.
|
|
5
5
|
Project-URL: Homepage, https://github.com/nishimotz/nextpytk
|
|
6
6
|
Project-URL: Repository, https://github.com/nishimotz/nextpytk
|
|
@@ -212,6 +212,7 @@ Layout().section("msg", side=Side.LEFT, fill=Fill.X)
|
|
|
212
212
|
| `Orient` | `Orient.HORIZONTAL/VERTICAL` | scale / paned orientation |
|
|
213
213
|
| `Relief` | `Relief.FLAT/RAISED/SUNKEN/...` | border style |
|
|
214
214
|
| `Justify` | `Justify.LEFT/RIGHT/CENTER` | text alignment |
|
|
215
|
+
| `Wrap` | `Wrap.WORD/NONE/CHAR` | text wrap mode |
|
|
215
216
|
| `SelectMode` | `SelectMode.SINGLE/BROWSE/MULTIPLE/EXTENDED` | listbox mode |
|
|
216
217
|
| `EventSeq` | `EventSeq.RETURN/ESCAPE/BACKSPACE/DELETE/TAB/...` | event sequences for bindings |
|
|
217
218
|
| `EventSeq` (mouse) | `EventSeq.BUTTON_1/2/3`, `DOUBLE_BUTTON_1/2/3`, `PRIMARY_DOUBLE_CLICK` | mouse event sequences |
|
|
@@ -480,6 +481,7 @@ Options:
|
|
|
480
481
|
| `fill` | `"x"` | Frame `fill` passed to `pack` |
|
|
481
482
|
| `expand` | `False` | Frame `expand` passed to `pack` |
|
|
482
483
|
| `sync_yscroll` | `False` | Wire the two widgets' y-scroll commands together |
|
|
484
|
+
| `line_numbers` | `False` | Add read-only line-number gutters to both sides |
|
|
483
485
|
| `side`, `padx`, `pady`, `anchor` | — | Standard `Layout` block placement options |
|
|
484
486
|
|
|
485
487
|
When `sync_yscroll=True` and both widgets are `@app.text` widgets, scrolling
|
|
@@ -488,6 +490,12 @@ achieved per-widget with `@app.text(..., sync_yscroll_with="other")`; paired
|
|
|
488
490
|
layout provides a layout-level switch that works without editing widget
|
|
489
491
|
declarations.
|
|
490
492
|
|
|
493
|
+
With `line_numbers=True` each side gains a read-only line-number gutter. Both
|
|
494
|
+
panes and both gutters share a **single vertical scrollbar**, so the gutters
|
|
495
|
+
stay in lock-step with the content (no drift). Logical line numbers
|
|
496
|
+
(`1..n`) follow the pane content; the right pane's numbers update as you edit.
|
|
497
|
+
|
|
498
|
+
|
|
491
499
|
### Cluster layout
|
|
492
500
|
|
|
493
501
|
`Layout.cluster()` is a wrapping flow: widgets are not stretched to fill a
|
|
@@ -512,6 +520,36 @@ rows automatically when the window is resized. Pass `gap=...` to override the
|
|
|
512
520
|
spacing default, and `side`/`fill`/`expand` to control how the cluster frame is
|
|
513
521
|
packed.
|
|
514
522
|
|
|
523
|
+
### Dynamic region switching (swap targets)
|
|
524
|
+
|
|
525
|
+
`Layout.target()` reserves a region whose contents change at runtime, mirroring
|
|
526
|
+
HTMX `hx-target` / `hx-swap`. Surrounding sections (toolbar, status) stay fixed
|
|
527
|
+
while only the target region swaps:
|
|
528
|
+
|
|
529
|
+
```python
|
|
530
|
+
layout = Layout().cluster("go_dir", "go_file", "info").target("main_area")
|
|
531
|
+
|
|
532
|
+
@app.swap(
|
|
533
|
+
"main_area",
|
|
534
|
+
variants={
|
|
535
|
+
"dir": [Layout().section("dir_tree")],
|
|
536
|
+
"file": [Layout().paired("left_text", "right_text",
|
|
537
|
+
fill=Fill.BOTH, expand=True)],
|
|
538
|
+
},
|
|
539
|
+
default="dir",
|
|
540
|
+
)
|
|
541
|
+
def main_area():
|
|
542
|
+
pass
|
|
543
|
+
|
|
544
|
+
# Switch imperatively at runtime:
|
|
545
|
+
app.swap_view("main_area", "file")
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
`@app.swap` variants are mounted up-front and shown/hidden via pack, so widget
|
|
549
|
+
state (e.g. a treeview selection or scroll position) survives switching. Each
|
|
550
|
+
variant is a `Layout` or a list of widget names. See
|
|
551
|
+
`examples/swap_demo.py`.
|
|
552
|
+
|
|
515
553
|
### Fluent DSL
|
|
516
554
|
|
|
517
555
|
**Pack sections:**
|
|
@@ -600,7 +638,7 @@ app.run(layout=b.build())
|
|
|
600
638
|
| `@app.combobox(name, values=..., values_key=..., readonly=..., font=...)` | ttk.Combobox | selected value `str` | `dict` |
|
|
601
639
|
| `@app.menubar(name)` | tk.Menu (window menubar) | — | menu item list |
|
|
602
640
|
| `@app.filepicker(name, mode=..., label=..., title=..., filetypes=..., ...)` | ttk.Button → tkinter.filedialog | selected path(s) `str`, `list[str]`, or `None` | `dict` |
|
|
603
|
-
| `@app.text(name, width=..., height=..., font=...)` | tk.Text | full content `str` | `dict` |
|
|
641
|
+
| `@app.text(name, width=..., height=..., font=..., wrap=..., h_scroll=...)` | tk.Text | full content `str` | `dict` |
|
|
604
642
|
| `@app.scale(name, from_=..., to=..., orient=...)` | ttk.Scale | value `str` | `dict` |
|
|
605
643
|
| `@app.spinbox(name, from_=..., to=..., values=..., font=...)` | ttk.Spinbox | value `str` | `dict` |
|
|
606
644
|
| `@app.listbox(name, items=..., items_key=..., selectmode=..., font=..., events=...)` | tk.Listbox | selected index `int` (`-1` if none) | `dict` |
|
|
@@ -651,6 +689,32 @@ Entry options: `placeholder`, `show`, `font`, `padding`, `width`, `state`.
|
|
|
651
689
|
Button, checkbutton, radiobutton, spinbox, combobox, listbox, text options:
|
|
652
690
|
- `font` is accepted uniformly. Under the hood, ttk widgets use a derived style so the rest of the theme (colors, maps, layout) is inherited.
|
|
653
691
|
|
|
692
|
+
Text options (continued):
|
|
693
|
+
- `wrap`: `Wrap.WORD` (default) / `Wrap.NONE` / `Wrap.CHAR`. `Wrap.NONE` keeps each logical line on a single row.
|
|
694
|
+
- `h_scroll`: when `True`, a horizontal scrollbar is added (and wired to `xscrollcommand`) so long lines are reachable, typically used with `wrap=Wrap.NONE`.
|
|
695
|
+
|
|
696
|
+
Every widget decorator also accepts `widget_kwargs: dict` for per-widget
|
|
697
|
+
design-token overrides applied after construction. Keys are widget-native
|
|
698
|
+
tk/ttk options (`padx`, `pady`, `bg`, `fg`, `font`, …). An invalid/unknown
|
|
699
|
+
key is ignored rather than aborting the build:
|
|
700
|
+
|
|
701
|
+
```python
|
|
702
|
+
@app.text("log", wrap="none", h_scroll=True,
|
|
703
|
+
widget_kwargs={"bg": "#1e1e1e", "fg": "#dcdcdc"})
|
|
704
|
+
def log(value): return {}
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
Runtime access: `app.text_widget(name)` returns the real `tk.Text`;
|
|
708
|
+
`app.layout_frame(name)` returns the layout section frame that owns a
|
|
709
|
+
widget (for manual grid/pack placement or re-parenting).
|
|
710
|
+
`app.on_text_set(name, hook)` registers a callback to run after a text
|
|
711
|
+
widget's content is replaced (used by paired line-number gutters).
|
|
712
|
+
|
|
713
|
+
Enum-like options (`wrap`, `state`, `orient`, `selectmode`, `mode`) are
|
|
714
|
+
validated at registration time: an invalid value raises a clear `ValueError`
|
|
715
|
+
naming the option and the allowed values, instead of failing later with a
|
|
716
|
+
`TclError` when the widget is built.
|
|
717
|
+
|
|
654
718
|
`@app.message` creates an auto-wrapping label. `width` sets initial pixel width; `auto_width=True` (default) tracks parent container resize.
|
|
655
719
|
|
|
656
720
|
---
|
|
@@ -741,6 +805,7 @@ uv run python examples/menubar_demo.py # menubar
|
|
|
741
805
|
uv run python examples/filepicker_demo.py # file picker
|
|
742
806
|
uv run python examples/disk_usage_flat_async.py # ncdu-style viewer (async)
|
|
743
807
|
uv run python examples/paired_demo.py # side-by-side paired layout with y-scroll sync
|
|
808
|
+
uv run python examples/swap_demo.py # dynamic region switching (Layout.target + @app.swap)
|
|
744
809
|
```
|
|
745
810
|
|
|
746
811
|
---
|
|
@@ -188,6 +188,7 @@ Layout().section("msg", side=Side.LEFT, fill=Fill.X)
|
|
|
188
188
|
| `Orient` | `Orient.HORIZONTAL/VERTICAL` | scale / paned orientation |
|
|
189
189
|
| `Relief` | `Relief.FLAT/RAISED/SUNKEN/...` | border style |
|
|
190
190
|
| `Justify` | `Justify.LEFT/RIGHT/CENTER` | text alignment |
|
|
191
|
+
| `Wrap` | `Wrap.WORD/NONE/CHAR` | text wrap mode |
|
|
191
192
|
| `SelectMode` | `SelectMode.SINGLE/BROWSE/MULTIPLE/EXTENDED` | listbox mode |
|
|
192
193
|
| `EventSeq` | `EventSeq.RETURN/ESCAPE/BACKSPACE/DELETE/TAB/...` | event sequences for bindings |
|
|
193
194
|
| `EventSeq` (mouse) | `EventSeq.BUTTON_1/2/3`, `DOUBLE_BUTTON_1/2/3`, `PRIMARY_DOUBLE_CLICK` | mouse event sequences |
|
|
@@ -456,6 +457,7 @@ Options:
|
|
|
456
457
|
| `fill` | `"x"` | Frame `fill` passed to `pack` |
|
|
457
458
|
| `expand` | `False` | Frame `expand` passed to `pack` |
|
|
458
459
|
| `sync_yscroll` | `False` | Wire the two widgets' y-scroll commands together |
|
|
460
|
+
| `line_numbers` | `False` | Add read-only line-number gutters to both sides |
|
|
459
461
|
| `side`, `padx`, `pady`, `anchor` | — | Standard `Layout` block placement options |
|
|
460
462
|
|
|
461
463
|
When `sync_yscroll=True` and both widgets are `@app.text` widgets, scrolling
|
|
@@ -464,6 +466,12 @@ achieved per-widget with `@app.text(..., sync_yscroll_with="other")`; paired
|
|
|
464
466
|
layout provides a layout-level switch that works without editing widget
|
|
465
467
|
declarations.
|
|
466
468
|
|
|
469
|
+
With `line_numbers=True` each side gains a read-only line-number gutter. Both
|
|
470
|
+
panes and both gutters share a **single vertical scrollbar**, so the gutters
|
|
471
|
+
stay in lock-step with the content (no drift). Logical line numbers
|
|
472
|
+
(`1..n`) follow the pane content; the right pane's numbers update as you edit.
|
|
473
|
+
|
|
474
|
+
|
|
467
475
|
### Cluster layout
|
|
468
476
|
|
|
469
477
|
`Layout.cluster()` is a wrapping flow: widgets are not stretched to fill a
|
|
@@ -488,6 +496,36 @@ rows automatically when the window is resized. Pass `gap=...` to override the
|
|
|
488
496
|
spacing default, and `side`/`fill`/`expand` to control how the cluster frame is
|
|
489
497
|
packed.
|
|
490
498
|
|
|
499
|
+
### Dynamic region switching (swap targets)
|
|
500
|
+
|
|
501
|
+
`Layout.target()` reserves a region whose contents change at runtime, mirroring
|
|
502
|
+
HTMX `hx-target` / `hx-swap`. Surrounding sections (toolbar, status) stay fixed
|
|
503
|
+
while only the target region swaps:
|
|
504
|
+
|
|
505
|
+
```python
|
|
506
|
+
layout = Layout().cluster("go_dir", "go_file", "info").target("main_area")
|
|
507
|
+
|
|
508
|
+
@app.swap(
|
|
509
|
+
"main_area",
|
|
510
|
+
variants={
|
|
511
|
+
"dir": [Layout().section("dir_tree")],
|
|
512
|
+
"file": [Layout().paired("left_text", "right_text",
|
|
513
|
+
fill=Fill.BOTH, expand=True)],
|
|
514
|
+
},
|
|
515
|
+
default="dir",
|
|
516
|
+
)
|
|
517
|
+
def main_area():
|
|
518
|
+
pass
|
|
519
|
+
|
|
520
|
+
# Switch imperatively at runtime:
|
|
521
|
+
app.swap_view("main_area", "file")
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
`@app.swap` variants are mounted up-front and shown/hidden via pack, so widget
|
|
525
|
+
state (e.g. a treeview selection or scroll position) survives switching. Each
|
|
526
|
+
variant is a `Layout` or a list of widget names. See
|
|
527
|
+
`examples/swap_demo.py`.
|
|
528
|
+
|
|
491
529
|
### Fluent DSL
|
|
492
530
|
|
|
493
531
|
**Pack sections:**
|
|
@@ -576,7 +614,7 @@ app.run(layout=b.build())
|
|
|
576
614
|
| `@app.combobox(name, values=..., values_key=..., readonly=..., font=...)` | ttk.Combobox | selected value `str` | `dict` |
|
|
577
615
|
| `@app.menubar(name)` | tk.Menu (window menubar) | — | menu item list |
|
|
578
616
|
| `@app.filepicker(name, mode=..., label=..., title=..., filetypes=..., ...)` | ttk.Button → tkinter.filedialog | selected path(s) `str`, `list[str]`, or `None` | `dict` |
|
|
579
|
-
| `@app.text(name, width=..., height=..., font=...)` | tk.Text | full content `str` | `dict` |
|
|
617
|
+
| `@app.text(name, width=..., height=..., font=..., wrap=..., h_scroll=...)` | tk.Text | full content `str` | `dict` |
|
|
580
618
|
| `@app.scale(name, from_=..., to=..., orient=...)` | ttk.Scale | value `str` | `dict` |
|
|
581
619
|
| `@app.spinbox(name, from_=..., to=..., values=..., font=...)` | ttk.Spinbox | value `str` | `dict` |
|
|
582
620
|
| `@app.listbox(name, items=..., items_key=..., selectmode=..., font=..., events=...)` | tk.Listbox | selected index `int` (`-1` if none) | `dict` |
|
|
@@ -627,6 +665,32 @@ Entry options: `placeholder`, `show`, `font`, `padding`, `width`, `state`.
|
|
|
627
665
|
Button, checkbutton, radiobutton, spinbox, combobox, listbox, text options:
|
|
628
666
|
- `font` is accepted uniformly. Under the hood, ttk widgets use a derived style so the rest of the theme (colors, maps, layout) is inherited.
|
|
629
667
|
|
|
668
|
+
Text options (continued):
|
|
669
|
+
- `wrap`: `Wrap.WORD` (default) / `Wrap.NONE` / `Wrap.CHAR`. `Wrap.NONE` keeps each logical line on a single row.
|
|
670
|
+
- `h_scroll`: when `True`, a horizontal scrollbar is added (and wired to `xscrollcommand`) so long lines are reachable, typically used with `wrap=Wrap.NONE`.
|
|
671
|
+
|
|
672
|
+
Every widget decorator also accepts `widget_kwargs: dict` for per-widget
|
|
673
|
+
design-token overrides applied after construction. Keys are widget-native
|
|
674
|
+
tk/ttk options (`padx`, `pady`, `bg`, `fg`, `font`, …). An invalid/unknown
|
|
675
|
+
key is ignored rather than aborting the build:
|
|
676
|
+
|
|
677
|
+
```python
|
|
678
|
+
@app.text("log", wrap="none", h_scroll=True,
|
|
679
|
+
widget_kwargs={"bg": "#1e1e1e", "fg": "#dcdcdc"})
|
|
680
|
+
def log(value): return {}
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
Runtime access: `app.text_widget(name)` returns the real `tk.Text`;
|
|
684
|
+
`app.layout_frame(name)` returns the layout section frame that owns a
|
|
685
|
+
widget (for manual grid/pack placement or re-parenting).
|
|
686
|
+
`app.on_text_set(name, hook)` registers a callback to run after a text
|
|
687
|
+
widget's content is replaced (used by paired line-number gutters).
|
|
688
|
+
|
|
689
|
+
Enum-like options (`wrap`, `state`, `orient`, `selectmode`, `mode`) are
|
|
690
|
+
validated at registration time: an invalid value raises a clear `ValueError`
|
|
691
|
+
naming the option and the allowed values, instead of failing later with a
|
|
692
|
+
`TclError` when the widget is built.
|
|
693
|
+
|
|
630
694
|
`@app.message` creates an auto-wrapping label. `width` sets initial pixel width; `auto_width=True` (default) tracks parent container resize.
|
|
631
695
|
|
|
632
696
|
---
|
|
@@ -717,6 +781,7 @@ uv run python examples/menubar_demo.py # menubar
|
|
|
717
781
|
uv run python examples/filepicker_demo.py # file picker
|
|
718
782
|
uv run python examples/disk_usage_flat_async.py # ncdu-style viewer (async)
|
|
719
783
|
uv run python examples/paired_demo.py # side-by-side paired layout with y-scroll sync
|
|
784
|
+
uv run python examples/swap_demo.py # dynamic region switching (Layout.target + @app.swap)
|
|
720
785
|
```
|
|
721
786
|
|
|
722
787
|
---
|
|
@@ -141,7 +141,76 @@ PyPI: Trusted Publishing on `v*` tag push (`.github/workflows/publish.yml`).
|
|
|
141
141
|
|
|
142
142
|
---
|
|
143
143
|
|
|
144
|
-
##
|
|
144
|
+
## Snapshot (v0.4.4) — text wrap/h-scroll, public runtime API
|
|
145
|
+
|
|
146
|
+
### Text widget ergonomics (continued)
|
|
147
|
+
|
|
148
|
+
- Added `wrap=` option to `@app.text` for logical-line display control:
|
|
149
|
+
- `Wrap.WORD` (default) — wrap at word boundaries.
|
|
150
|
+
- `Wrap.NONE` — keep each logical line on one row (requires horizontal scrolling).
|
|
151
|
+
- `Wrap.CHAR` — wrap at any character boundary.
|
|
152
|
+
- Added a new `nextpytk.types.Wrap` typed-constant class and `WrapLike` union.
|
|
153
|
+
- Added `h_scroll=` option to `@app.text`: when True, a horizontal scrollbar is added (and wired to `xscrollcommand`) so long lines are reachable in `Wrap.NONE` mode. The horizontal scrollbar is tracked in `app._text_hscrollbars`.
|
|
154
|
+
|
|
155
|
+
### Decorator type-definition sync
|
|
156
|
+
|
|
157
|
+
- `TreeviewOptions.double_click` is now typed and honored by `@app.treeview`.
|
|
158
|
+
- Defaults to `True`: the `activate` handler fires on double-click (previous behavior).
|
|
159
|
+
- When `False`, `activate` fires on a single `ButtonRelease-1` instead.
|
|
160
|
+
- `_on_treeview_activate` accepts an optional `event` argument so the single-click binding can reuse it.
|
|
161
|
+
|
|
162
|
+
### Public runtime API
|
|
163
|
+
|
|
164
|
+
- Added `app.layout_frame(name)` — returns the layout frame that owns a widget (the section frame), so applications can place/re-parent widgets with grid/pack without reaching into the private `_widget_masters`.
|
|
165
|
+
- This is the public replacement for the internal `_widget_masters` access that previously forced applications to depend on private state.
|
|
166
|
+
|
|
167
|
+
### Per-widget design-token overrides
|
|
168
|
+
|
|
169
|
+
- All widget decorators now accept `widget_kwargs: dict[str, Any]` (on `CommonWidgetOptions`).
|
|
170
|
+
- Keys are widget-native tk/ttk options (`padx`, `pady`, `bg`, `fg`, `font`, …); values are the native values.
|
|
171
|
+
- Overrides are applied after construction by `_apply_widget_overrides()`. An invalid/unknown key raises no error: a `TclError` is swallowed so one bad key does not abort the build.
|
|
172
|
+
- This removes the need to monkey-patch the shared `tokens` module to tweak a single widget's appearance.
|
|
173
|
+
|
|
174
|
+
### Decorator argument validation
|
|
175
|
+
|
|
176
|
+
- Added `_validate_choice()` / `_validate_positive_int()` helpers used by the decorators to reject invalid enum-like and non-positive-int options at **registration time** (before any Tk widget is built).
|
|
177
|
+
- Validated options:
|
|
178
|
+
- `text`: `state` (`normal`/`disabled`/`active`), `wrap` (`word`/`none`/`char`)
|
|
179
|
+
- `scale`, `progressbar`, `paned`: `orient` (`horizontal`/`vertical`)
|
|
180
|
+
- `listbox`, `treeview`: `selectmode` (`single`/`browse`/`multiple`/`extended`)
|
|
181
|
+
- `filepicker`: `mode` (`open`/`open_multiple`/`save`/`directory`)
|
|
182
|
+
- `progressbar`: `mode` (`determinate`/`indeterminate`)
|
|
183
|
+
- A mistyped value (e.g. `wrap="wrap"`) now raises a clear `ValueError` naming the option, widget, and allowed values — instead of a cryptic `_tkinter.TclError` at runtime.
|
|
184
|
+
|
|
185
|
+
### Paired line-number gutters
|
|
186
|
+
|
|
187
|
+
- Added `line_numbers=True` to `Layout.paired(...)`.
|
|
188
|
+
- Each side gains a read-only line-number gutter (`disabled` tk.Text, `takefocus=0`, arrow cursor).
|
|
189
|
+
- **Reliable sync via a single shared vertical scrollbar**: both panes and both gutters drive one shared scrollbar. Each widget's `yscrollcommand` is chained so scrolling any widget moves the other three, and the shared scrollbar's `command` drives all four. This avoids the `yview_moveto` drift that plagued per-widget gutter chaining.
|
|
190
|
+
- Gutters stay in sync when content changes via `app.text_set()` (a new `app.on_text_set()` hook) and when the editable right pane is typed into (`<KeyRelease>`).
|
|
191
|
+
- Added `examples/paired_demo.py` demonstrating `line_numbers=True`.
|
|
192
|
+
- The new `app.on_text_set(name, hook)` public helper lets layout features react to programmatic text replacement.
|
|
193
|
+
|
|
194
|
+
### Dynamic layout switching (swap targets)
|
|
195
|
+
|
|
196
|
+
- Added `Layout.target(name)` to reserve a swap region whose contents change at runtime (mirrors HTMX `hx-target`).
|
|
197
|
+
- Added `@app.swap(name, variants=..., default=...)` to declare the layouts that can fill a target (mirrors HTMX `hx-swap`), and `app.swap_view(name, variant)` to switch imperatively.
|
|
198
|
+
- Variants are mounted up-front and shown/hidden via `pack`/`pack_forget`, so widget state (treeview selection, scroll position) survives switching. Surrounding `Layout.section` blocks (toolbar, status) stay fixed.
|
|
199
|
+
- Added `examples/swap_demo.py` (folder view <-> paired diff view).
|
|
200
|
+
|
|
201
|
+
### Tests
|
|
202
|
+
|
|
203
|
+
- Added `tests/test_widget_public_api.py` covering `layout_frame` and `widget_kwargs` (applied, ignored-invalid).
|
|
204
|
+
- Added `tests/test_decorator_validation.py` covering invalid-option rejection for every validated widget.
|
|
205
|
+
- Added wrap/h-scroll tests to `tests/test_text.py`.
|
|
206
|
+
- Added `double_click` tests to `tests/test_state.py`.
|
|
207
|
+
- Added paired line-number gutter tests to `tests/test_paired.py` (creation, population, shared-scrollbar sync).
|
|
208
|
+
- Added swap tests to `tests/test_swap.py` (variant registration, build, switching, back-and-forth, pre-build intent).
|
|
209
|
+
- Suite grew from 181 to 208 passing; pyright clean.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Near term (post-0.4.4)
|
|
145
214
|
|
|
146
215
|
### A11y
|
|
147
216
|
|
|
@@ -158,9 +227,11 @@ PyPI: Trusted Publishing on `v*` tag push (`.github/workflows/publish.yml`).
|
|
|
158
227
|
### Widget expansion
|
|
159
228
|
|
|
160
229
|
- [x] Per-widget `bind` for entry and listbox (`events=` on `@app.entry` and `@app.listbox`)
|
|
230
|
+
- [x] Per-widget `bind` for other widgets via `widget_kwargs` / widget-level options
|
|
161
231
|
- [ ] Per-widget `bind` for other widgets (`text`, `combobox`, `treeview`, `button`, etc.)
|
|
162
232
|
- [x] listbox callback value design: keep selection index (unify with `treeview`)
|
|
163
233
|
- [x] `@app.filepicker` — `filedialog` wrapper (schema/tool naming still open)
|
|
234
|
+
- [x] Text `wrap=` / `h_scroll=` options (v0.4.4)
|
|
164
235
|
|
|
165
236
|
### Layout DSL
|
|
166
237
|
|
|
@@ -169,6 +240,8 @@ PyPI: Trusted Publishing on `v*` tag push (`.github/workflows/publish.yml`).
|
|
|
169
240
|
- [x] Nested frames (`Layout` inside `Layout`) via `Layout.frame(...)`
|
|
170
241
|
- [x] Global default for `padx`/`pady` via `Layout(spacing=...)`
|
|
171
242
|
- [x] `Layout.cluster(...)` wrapping flow; `Layout.paired(..., sync_yscroll=)`
|
|
243
|
+
- [x] `Layout.paired(..., line_numbers=True)` read-only line-number gutters (v0.4.4)
|
|
244
|
+
- [x] `Layout.target(...)` + `@app.swap(...)` dynamic region switching (v0.4.4)
|
|
172
245
|
|
|
173
246
|
### Agent / LLM integration
|
|
174
247
|
|