feat(tui): add SquidStd.Tui — MVVM layer over Terminal.Gui v2#14
Merged
Conversation
…unityToolkit.Mvvm
…extField, Button)
- Add ConventionNames internal helper that strips known widget suffixes (Field, Label, Button, Text, View, Box, List) from a widget Id to derive the ViewModel property name, and appends "Command" for ICommand members - Add ViewBinder.AutoBind partial that iterates View.SubViews and convention-binds each named subview to the matching VM member: Button → ICommand property via Command(), TextField → string property two-way (OneWay + ValueChanged write-back), Label → string property one-way - Add ConventionNamesTests (5 cases: 4 MemberName theory + 1 CommandName)
- Add TuiApplicationHost: boots Terminal.Gui via Application.Init/Run(shell)/Shutdown; creates a borderless full-screen Window shell before NavigateToAsync so the first screen is added to a non-null container (fixes the null Application.TopRunnableView race under Terminal.Gui 2.4.16 which removed Application.Top/Toplevel) - Revise TerminalGuiViewHost: replace Application.TopRunnableView usage with an explicit View? Container property set by TuiApplicationHost; Show/Remove operate on the guaranteed-non-null shell, sizing each child with Dim.Fill() - Add RegisterTuiExtensions (C# 14 extension block on IContainer): RegisterTui() wires TuiViewRegistry, TerminalGuiViewHost (singleton), ITuiViewHost mapped to the same concrete singleton via RegisterMapping<,>, ITuiNavigator, TuiApplicationHost; RegisterView<TView,TViewModel>() registers the pair transient and maps in the registry - Add DI tests RegisterTuiExtensionsTests (2 tests, TDD: written before production code) - Add README.md for SquidStd.Tui
- add samples/SquidStd.Samples.Tui with CounterViewModel, CounterView, Program.cs - register SquidStd.Samples.Tui in SquidStd.slnx under /samples/ folder - add TerminalGuiViewHostTests: headless Show/Remove coverage for blank-screen regression - harden RegisterTui_RegistersNavigatorAndRegistry with Assert.Same singleton guard (TerminalGuiViewHost and ITuiViewHost must be the same instance)
…dead code - TuiNavigator: deactivate the covered screen before pushing a new one so OnActivatedAsync/OnDeactivatedAsync are paired (no double-activate on forward-then-back navigation) - add ViewBinderAutoBindTests covering the reflection binding paths (label one-way, textfield two-way apply, button command discovery, unrecognised/wrong-type skip) - delete unused ViewBinder.Track method - add missing XML summaries on ViewBinder ctor/Dispose, TuiViewRegistry Map/ViewTypeFor and TerminalGuiViewHost Show/Remove
| [Fact] | ||
| public void RegisterView_MapsAndResolvesBothTypes() | ||
| { | ||
| var container = new Container(); |
| public async Task RunAsync<TRootViewModel>() | ||
| where TRootViewModel : TuiViewModel | ||
| { | ||
| Application.Init(); |
|
|
||
| await _navigator.NavigateToAsync<TRootViewModel>(); | ||
|
|
||
| Application.Run(shell); |
| finally | ||
| { | ||
| _viewHost.Container = null; | ||
| Application.Shutdown(); |
| [Fact] | ||
| public void RegisterTui_RegistersNavigatorAndRegistry() | ||
| { | ||
| var container = new Container(); |
| private string _title = string.Empty; | ||
|
|
||
| [ObservableProperty] | ||
| private string _name = string.Empty; |
| private sealed partial class AutoBindViewModel : ObservableObject | ||
| { | ||
| [ObservableProperty] | ||
| private string _title = string.Empty; |
Comment on lines
+10
to
+16
| foreach (var suffix in _suffixes) | ||
| { | ||
| if (widgetId.Length > suffix.Length && widgetId.EndsWith(suffix, StringComparison.Ordinal)) | ||
| { | ||
| return widgetId[..^suffix.Length]; | ||
| } | ||
| } |
| private string _title = "SquidStd.Tui — Counter"; | ||
|
|
||
| [ObservableProperty] | ||
| private string _value = "0"; |
| public sealed partial class CounterViewModel : TuiViewModel | ||
| { | ||
| [ObservableProperty] | ||
| private string _title = "SquidStd.Tui — Counter"; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
New module SquidStd.Tui: a thin MVVM layer over Terminal.Gui 2.4.16 + CommunityToolkit.Mvvm + DryIoc. The logic-heavy core (binder, property paths, reentry guard, navigator, DI) is Terminal.Gui-agnostic and unit-tested; the thin Terminal.Gui-coupled layer (widget overloads,
TuiView<T>, view host, app host) is build- and sample-verified.What's included
ViewBinder(one partial type) —OneWay/TwoWay/Commandcores, typed expression overloads, Terminal.Gui widget overloads (OneWayText/OneWayTitle/TwoWay(TextField)/Command(Button)), and opt-in conventionAutoBind. Two-way uses aReentryGuardto stop VM→widget→VM feedback loops; subscriptions are released onDispose.TuiViewModel—ObservableObjectbase withNavigatoraccess and pairedOnActivatedAsync/OnDeactivatedAsynclifecycle hooks.ITuiNavigator/TuiNavigator,ITuiView,ITuiViewHost,TuiViewRegistry.TuiView<TViewModel> : Window,TerminalGuiViewHost,TuiApplicationHost(boots the Terminal.Gui loop with a shell container so the first screen renders under the 2.4.16 Runnable model).RegisterTui()/RegisterView<TView,TViewModel>()DryIoc extensions.samples/SquidStd.Samples.Tui(runnable Counter) + README.Notes
Toplevelremoved,Application.Top→Application.TopRunnableView, widgets inTerminal.Gui.Views,ViewinTerminal.Gui.ViewBase,TextField.ValueChanged(notTextChanged),Button.Accepted(notAccepting).Test plan
dotnet runof the Counter sample in a real terminal (interactive event loop, keyboard, rendering)