From f0fd364b68b5de7236f57366e014e6dfe6959100 Mon Sep 17 00:00:00 2001 From: Cass Deckard Date: Sun, 1 Mar 2026 21:42:15 -0800 Subject: [PATCH 1/3] Fix #5 --- builder/properties.go | 12 ++++++ builder/properties_test.go | 74 ++++++++++++++++++++++++++++++++++++ config/types.go | 2 + docs/tview-coverage.md | 6 ++- example/config/textview.yaml | 9 ++++- 5 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 builder/properties_test.go diff --git a/builder/properties.go b/builder/properties.go index 88bf38c..e5f865b 100644 --- a/builder/properties.go +++ b/builder/properties.go @@ -145,6 +145,18 @@ func (pm *PropertyMapper) applyTextViewProperties(tv *tview.TextView, prim *conf } }) } + if prim.Regions && prim.OnHighlighted != "" && pm.executor != nil { + onHighlightedExpr := prim.OnHighlighted + tv.SetHighlightedFunc(func(added, removed, remaining []string) { + if len(added) == 0 { + return + } + pm.context.SetStateDirect("__highlightedRegion", added[0]) + if cb, err := pm.executor.ExecuteCallback(onHighlightedExpr); err == nil { + cb() + } + }) + } return nil } diff --git a/builder/properties_test.go b/builder/properties_test.go new file mode 100644 index 0000000..01f96bd --- /dev/null +++ b/builder/properties_test.go @@ -0,0 +1,74 @@ +package builder + +import ( + "testing" + + "github.com/cassdeckard/tviewyaml/config" + "github.com/cassdeckard/tviewyaml/template" + "github.com/rivo/tview" +) + +func TestApplyTextViewProperties_OnHighlighted(t *testing.T) { + app := tview.NewApplication() + pages := tview.NewPages() + ctx := template.NewContext(app, pages) + registry := template.NewFunctionRegistry() + executor := template.NewExecutor(ctx, registry) + pm := NewPropertyMapper(ctx, executor) + + tv := tview.NewTextView() + tv.SetRegions(true) + tv.SetText(`["a"]Region A[""]`) + + prim := &config.Primitive{ + Type: "textView", + Regions: true, + OnHighlighted: `{{ showNotification "highlighted" }}`, + } + + err := pm.ApplyProperties(tv, prim) + if err != nil { + t.Fatalf("ApplyProperties: %v", err) + } + // Wiring succeeded; SetHighlightedFunc is attached. The callback runs when + // tview draws after a highlight change (tested via acceptance if needed). +} + +func TestBuildFlex_TextViewWithOnHighlighted(t *testing.T) { + app := tview.NewApplication() + pages := tview.NewPages() + ctx := template.NewContext(app, pages) + registry := template.NewFunctionRegistry() + b := NewBuilder(ctx, registry) + + pageConfig := &config.PageConfig{ + Type: "flex", + Items: []config.FlexItem{ + { + Primitive: &config.Primitive{ + Type: "textView", + Regions: true, + DynamicColors: true, + Text: `["slide1"]Slide 1[""] ["slide2"]Slide 2[""]`, + OnHighlighted: `{{ switchToPage "main" }}`, + }, + FixedSize: 0, + Proportion: 1, + Focus: true, + }, + }, + } + + result, err := b.BuildFromConfig(pageConfig) + if err != nil { + t.Fatalf("BuildFromConfig: %v", err) + } + + flex, ok := result.(*tview.Flex) + if !ok { + t.Fatalf("expected *tview.Flex, got %T", result) + } + if flex.GetItemCount() != 1 { + t.Errorf("GetItemCount() = %d, want 1", flex.GetItemCount()) + } +} diff --git a/config/types.go b/config/types.go index fe09305..954c1fb 100644 --- a/config/types.go +++ b/config/types.go @@ -95,6 +95,8 @@ type Primitive struct { OnCancel string `yaml:"onCancel,omitempty"` // Template expression when form is cancelled (Escape); if unset and OnSubmit set, Escape runs OnSubmit // onDone: Template expression when user presses Enter/Escape (TextView, InputField, Table) OnDone string `yaml:"onDone,omitempty"` + // onHighlighted: Template expression when highlighted region changes (TextView with regions; state: __highlightedRegion) + OnHighlighted string `yaml:"onHighlighted,omitempty"` // Table-specific properties OnCellSelected string `yaml:"onCellSelected,omitempty"` // Template expression when a cell is selected (state: __selectedCellText, __selectedRow, __selectedCol) Borders bool `yaml:"borders,omitempty"` // Show borders between cells diff --git a/docs/tview-coverage.md b/docs/tview-coverage.md index d59a571..9fefc9d 100644 --- a/docs/tview-coverage.md +++ b/docs/tview-coverage.md @@ -19,7 +19,7 @@ This table maps [tview](https://pkg.go.dev/github.com/rivo/tview) primitives and | **Pages** | Yes | Yes (root) | Yes | [app.yaml](../example/config/app.yaml), [nested-pages.yaml](../example/config/nested-pages.yaml) | Tab-like page switching; `ref` to YAML files | | **Table** | Yes | Yes | Yes | [table.yaml](../example/config/table.yaml) | Headers, rows, borders, fixed rows/columns; `onDone` for Enter/Escape | | **TextArea** | Yes | No | Yes (Form item) | [form.yaml](../example/config/form.yaml) | Form item type `textarea`; multi-line input | -| **TextView** | Yes | No | Yes | [textview.yaml](../example/config/textview.yaml) | Dynamic colors, regions, scrollable; `onDone` for Enter/Escape | +| **TextView** | Yes | No | Yes | [textview.yaml](../example/config/textview.yaml) | Dynamic colors, regions, scrollable; `onDone` for Enter/Escape; `onHighlighted` for region clicks | | **TreeView** | Yes | Yes | Yes | [treeview.yaml](../example/config/treeview.yaml), [treeview-standalone.yaml](../example/config/treeview-standalone.yaml), [treeview-modes.yaml](../example/config/treeview-modes.yaml) | Nodes with children; selectable modes | ## Form Item Types @@ -48,6 +48,10 @@ TextView, InputField (standalone), and Table support `onDone`β€”a template expre Note: tview's Table does not call SetDoneFunc for Enter when rows are selectableβ€”Enter is used for selection. Escape still triggers onDone on selectable tables. +## Callback Support: onHighlighted (TextView) + +TextView with `regions: true` supports `onHighlighted`β€”a template expression that runs when the highlighted region changes (from clicks or Tab/Enter navigation). State `__highlightedRegion` is set to the first newly highlighted region ID before the callback runs. Use `switchToPage "{{ bindState __highlightedRegion }}"` for presentation-style info bar navigation. See [textview.yaml](../example/config/textview.yaml). + ## Additional Example Configs (Feature Demos) | Config | Purpose | diff --git a/example/config/textview.yaml b/example/config/textview.yaml index 9b8a99d..b236476 100644 --- a/example/config/textview.yaml +++ b/example/config/textview.yaml @@ -18,6 +18,7 @@ items: title: "TextView with Colors and Regions" dynamicColors: true regions: true + onHighlighted: '{{ showNotification "Region selected" }}' text: | [yellow]TextView Features:[white] @@ -40,7 +41,11 @@ items: type: textView textAlign: center textColor: gray - text: "Enter to start selection, Tab/Backtab to navigate regions, arrow keys to scroll, ESC to return" - fixedSize: 2 + dynamicColors: true + text: | + Enter to start selection, Tab/Backtab to navigate regions, arrow keys to scroll, ESC to return + + [green]Status:[white] {{ bindState notification }} + fixedSize: 4 proportion: 1 focus: false From 1ad5a27fc2aca75a005f2bef86553c7e6ef282bc Mon Sep 17 00:00:00 2001 From: Cass Deckard Date: Sun, 1 Mar 2026 21:42:35 -0800 Subject: [PATCH 2/3] Show region number in notification --- example/config/textview.yaml | 2 +- template/builtins.go | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/example/config/textview.yaml b/example/config/textview.yaml index b236476..f15964e 100644 --- a/example/config/textview.yaml +++ b/example/config/textview.yaml @@ -18,7 +18,7 @@ items: title: "TextView with Colors and Regions" dynamicColors: true regions: true - onHighlighted: '{{ showNotification "Region selected" }}' + onHighlighted: '{{ showHighlightedNotification }}' text: | [yellow]TextView Features:[white] diff --git a/template/builtins.go b/template/builtins.go index 828d7d6..71675af 100644 --- a/template/builtins.go +++ b/template/builtins.go @@ -28,6 +28,17 @@ func registerBuiltinFunctions(registry *FunctionRegistry) { ctx.SetStateDirect("notification", msg) }) + // showHighlightedNotification: sets notification to "Region X selected" using __highlightedRegion. + // For use in onHighlighted callbacks to show which region was selected. + registry.Register("showHighlightedNotification", 0, intPtr(0), nil, func(ctx *Context) { + region, ok := ctx.GetState("__highlightedRegion") + msg := "Region selected" + if ok && fmt.Sprint(region) != "" { + msg = fmt.Sprintf("Region %s selected", region) + } + ctx.SetStateDirect("notification", msg) + }) + // switchToPage: switches to a different page registry.Register("switchToPage", 1, intPtr(1), nil, func(ctx *Context, pageName string) { if ctx.Pages != nil { From 0e1bb308d135164d5db024ecb5d1cb0f82c1ce62 Mon Sep 17 00:00:00 2001 From: Cass Deckard Date: Sun, 1 Mar 2026 21:45:21 -0800 Subject: [PATCH 3/3] fix: acceptance tests --- .../TestAcceptance/KeyNavigation_TextViewPage.terminal | 4 ++-- .../TestAcceptance/KeyNavigation_TextViewPage.terminal | 6 +++--- .../TestAcceptance/KeyNavigation_TextViewPage.terminal | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/example/acceptance/testdata/snapshots/120x30/TestAcceptance/KeyNavigation_TextViewPage.terminal b/example/acceptance/testdata/snapshots/120x30/TestAcceptance/KeyNavigation_TextViewPage.terminal index ac4800c..e06c0e0 100644 --- a/example/acceptance/testdata/snapshots/120x30/TestAcceptance/KeyNavigation_TextViewPage.terminal +++ b/example/acceptance/testdata/snapshots/120x30/TestAcceptance/KeyNavigation_TextViewPage.terminal @@ -23,8 +23,8 @@ β•‘ β•‘ β•‘ β•‘ β•‘ β•‘ -β•‘ β•‘ -β•‘ β•‘ β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•  Enter to start selection, Tab/Backtab to navigate regions, arrow keys to scroll, ESC to return + + Status:   \ No newline at end of file diff --git a/example/acceptance/testdata/snapshots/40x10/TestAcceptance/KeyNavigation_TextViewPage.terminal b/example/acceptance/testdata/snapshots/40x10/TestAcceptance/KeyNavigation_TextViewPage.terminal index 95d6d07..63e9e78 100644 --- a/example/acceptance/testdata/snapshots/40x10/TestAcceptance/KeyNavigation_TextViewPage.terminal +++ b/example/acceptance/testdata/snapshots/40x10/TestAcceptance/KeyNavigation_TextViewPage.terminal @@ -3,8 +3,8 @@  Text display with dynamic colors,   regions, highlights, and scrolling ╔═══TextView with Colors and Regions═══╗ -β•‘TextView Features: β•‘ -β•‘ β•‘ β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•  Enter to start selection, Tab/Backtab  - to navigate regions, arrow keys to  \ No newline at end of file + to navigate regions, arrow keys to  + scroll, ESC to return + \ No newline at end of file diff --git a/example/acceptance/testdata/snapshots/80x24/TestAcceptance/KeyNavigation_TextViewPage.terminal b/example/acceptance/testdata/snapshots/80x24/TestAcceptance/KeyNavigation_TextViewPage.terminal index f2279e7..402ea65 100644 --- a/example/acceptance/testdata/snapshots/80x24/TestAcceptance/KeyNavigation_TextViewPage.terminal +++ b/example/acceptance/testdata/snapshots/80x24/TestAcceptance/KeyNavigation_TextViewPage.terminal @@ -17,8 +17,8 @@ β•‘ β•‘ β•‘4. Word Wrap: Long lines automatically wrap to fit the view β•‘ β•‘ β•‘ -β•‘Press Enter to start region selection, Tab/Backtab to navigate regions β•‘ -β•‘ β•‘ β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•  Enter to start selection, Tab/Backtab to navigate regions, arrow keys to  - scroll, ESC to return \ No newline at end of file + scroll, ESC to return + + Status:  \ No newline at end of file