Skip to content

feat(tui): add SquidStd.Tui — MVVM layer over Terminal.Gui v2#14

Merged
tgiachi merged 14 commits into
developfrom
feature/tui-mvvm
Jun 29, 2026
Merged

feat(tui): add SquidStd.Tui — MVVM layer over Terminal.Gui v2#14
tgiachi merged 14 commits into
developfrom
feature/tui-mvvm

Conversation

@tgiachi

@tgiachi tgiachi commented Jun 29, 2026

Copy link
Copy Markdown
Owner

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 / Command cores, typed expression overloads, Terminal.Gui widget overloads (OneWayText/OneWayTitle/TwoWay(TextField)/Command(Button)), and opt-in convention AutoBind. Two-way uses a ReentryGuard to stop VM→widget→VM feedback loops; subscriptions are released on Dispose.
  • TuiViewModelObservableObject base with Navigator access and paired OnActivatedAsync/OnDeactivatedAsync lifecycle hooks.
  • Navigation — ViewModel-first stack: ITuiNavigator/TuiNavigator, ITuiView, ITuiViewHost, TuiViewRegistry.
  • HostingTuiView<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).
  • DIRegisterTui() / RegisterView<TView,TViewModel>() DryIoc extensions.
  • Samplesamples/SquidStd.Samples.Tui (runnable Counter) + README.

Notes

  • Targets Terminal.Gui 2.4.16 (GA). Adapted to its API changes vs older v2 docs: Toplevel removed, Application.TopApplication.TopRunnableView, widgets in Terminal.Gui.Views, View in Terminal.Gui.ViewBase, TextField.ValueChanged (not TextChanged), Button.Accepted (not Accepting).
  • Activation hooks are paired: forward navigation deactivates the covered screen before activating the new one.

Test plan

  • Full solution builds (0 errors)
  • Full suite green — 906 tests, 0 failures (878 pre-existing + 28 new Tui tests)
  • Sample project builds against Terminal.Gui 2.4.16
  • Headless host wiring smoke test (Show attaches to container / Remove detaches)
  • Manual dotnet run of the Counter sample in a real terminal (interactive event loop, keyboard, rendering)

tgiachi added 14 commits June 29, 2026 19:21
- 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
@tgiachi
tgiachi merged commit 1ef7736 into develop Jun 29, 2026
2 checks passed
@tgiachi
tgiachi deleted the feature/tui-mvvm branch June 29, 2026 18:53
[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";
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants