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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nextpytk
3
- Version: 0.4.3
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
- ## Near term (post-0.4.3)
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
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "nextpytk"
7
- version = "0.4.3"
7
+ version = "0.4.4"
8
8
  description = "Accessible, declarative Tkinter applications from ordinary Python functions."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"