Skip to content

Release: TUI MVVM + DSL, password-based encryption, and S3/database VFS backends#23

Merged
tgiachi merged 48 commits into
mainfrom
develop
Jun 30, 2026
Merged

Release: TUI MVVM + DSL, password-based encryption, and S3/database VFS backends#23
tgiachi merged 48 commits into
mainfrom
develop

Conversation

@tgiachi

@tgiachi tgiachi commented Jun 30, 2026

Copy link
Copy Markdown
Owner

Release summary

Rolls up this development cycle from develop into main (triggers semantic-release + NuGet publish).

Highlights

  • SquidStd.Tui — new module: MVVM for terminal apps over Terminal.Gui v2 (observable ViewModels, fluent ViewBinder + AutoBind, ViewModel-first navigation, DryIoc wiring) plus a declarative DSL (TuiComposedView<T> / Ui.VStack/Ui.Label/Ui.Button).
  • SquidStd.Crypto password-based encryptionPasswordCipher (Argon2id → AES-256-GCM, self-describing versioned envelope) with PbkdfCost presets.
  • VFS backends & decoratorsSquidStd.Vfs.S3 (S3-compatible via MinIO SDK), SquidStd.Vfs.Database (FreeSql), and composable decorators ReadOnlyFileSystem / ScopedFileSystem / OverlayFileSystem / CachingFileSystem (offline-resilient reads).

Also

  • Fixes: disposable-transient view registration (TUI sample), flaky FileWatcher debounce test.
  • Maintenance: repo-wide formatting sweep, Redis + AWS SDK package updates.
  • Docs: site-level documentation for all of the above (root index, DocFX articles, tutorials).

Merge note

Merge with a merge commit (not squash) so semantic-release sees the conventional commits and publishes to NuGet.

Test plan

  • Full suite green on develop — 971 tests, 0 failures (incl. MinIO + SQLite integration)
  • CI green on every merged PR

tgiachi added 30 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
feat(tui): add SquidStd.Tui — MVVM layer over Terminal.Gui v2
- Add BindMode enum (OneWay/TwoWay) under SquidStd.Tui.Types.Tui
- Add StackOrientation enum (Vertical/Horizontal) under SquidStd.Tui.Types.Tui
- Add abstract TuiNode<TViewModel> base class under SquidStd.Tui.Dsl
- Add LabelNode<TViewModel>: one-way string property binding
- Add TextFieldNode<TViewModel>: string property binding with BindMode
- Add ButtonNode<TViewModel>: caption + ICommand expression binding
- Add StackNode<TViewModel>: orientation + typed children list
- Add TuiNodeTests covering all four node types (4 tests, all green)
… views

- add TuiNodeMaterializer in SquidStd.Tui.Dsl; walks TuiNode<TViewModel> trees and produces View graphs
- wire LabelNode via binder.OneWayText, TextFieldNode via TwoWay/OneWay binder overloads, ButtonNode via binder.Command
- StackNode (Vertical/Horizontal) produces a container View using Pos.Bottom/Pos.Right and Dim.Fill for layout
- TViewModel constrained to INotifyPropertyChanged so existing binder overloads resolve without new binding logic
- add TuiNodeMaterializerTests covering VStack+Label one-way propagation and TextField two-way VM→widget; both pass headless
- outer test class marked partial because nested ObservableObject partial class requires it (CommunityToolkit source generator)
feat(tui): add fluent declarative UI DSL for SquidStd.Tui
Views derive from Window (IDisposable). DryIoc throws
RegisteredDisposableTransientWontBeDisposedByContainer when a disposable type
is registered as a transient, so resolving the application host and navigating
to the first screen crashed at startup. Register the view with
Setup.With(allowDisposableTransient: true): the view host already disposes each
view on Remove, so the container must not also track/dispose it. Add a
regression test registering a disposable view through RegisterView.
fix(tui): allow disposable transient views in RegisterView
Watch_RapidWritesToSameFile_DebouncesToSingleEvent assumed five rapid writes
fall inside one 200ms debounce window; on a loaded CI runner a scheduler/GC
pause between writes split the burst across two windows, emitting two events and
failing Assert.Single. Use a 1s debounce window for this test and a tight
synchronous write loop so the burst reliably collapses to a single event without
weakening the assertion's intent.
test(core): stabilise flaky FileWatcher debounce test
Apply the configured ReSharper/formatter layout across the solution (attribute
placement, member layout, whitespace). One semantic side-effect was corrected:
the cleanup turned a test helper property get-only, breaking
PropertyPathTests.Setter_Property_WritesValue (the property is written via an
expression-tree setter the analyzer can't see) — restored its setter. No
production property lost a setter (verified) and the full suite is green.
style: apply repo-wide code formatting
- StackExchange.Redis 3.0.7 -> 3.0.11
- AWSSDK.SimpleNotificationService / AWSSDK.SQS 4.0.3.8 -> 4.0.100
- AWSSDK.KeyManagementService 4.0.12.9 -> 4.0.100
- AWSSDK.SecretsManager 4.0.5.8 -> 4.0.100

Build clean and full suite green (917) with no new deprecation warnings in the
affected adapters.
chore(deps): update Redis and AWS SDK packages
tgiachi added 18 commits June 30, 2026 14:32
…+ AES-GCM)

- Add PasswordCipher static class: Encrypt/Decrypt byte[], EncryptString/DecryptString (base64)
- Add PasswordDecryptionException (sealed, extends CryptographicException) for wrong password or tampered data
- AES-256-GCM with AAD-bound authentication; wrong password and ciphertext/header tampering all throw PasswordDecryptionException
- Encrypt produces unique blobs per call via random salt + nonce
- PbkdfCost defaults to Moderate when omitted
The envelope stores parallelism in a single byte, so a value above 255 would
truncate silently and produce an undecryptable blob (the key is derived with the
real lane count on encrypt but the envelope records the truncated one). Guard it
with ArgumentOutOfRangeException.ThrowIfGreaterThan up front. Add tests for the
guard plus empty-plaintext and non-ASCII-password round-trips.
feat(crypto): add password-based encryption (PasswordCipher)
- new project SquidStd.Vfs.S3 (net10.0, PublishNuget=true, Minio 7.0.0)
- S3FileSystemOptions: Aws (AwsConfigEntry) + Bucket, mirrors S3StorageOptions shape
- S3FileSystem: IVirtualFileSystem + IDisposable over MinIO client
  - lazy EnsureBucketAsync (BucketExistsArgs / MakeBucketArgs) with SemaphoreSlim
  - ExistsAsync via StatObjectArgs, ObjectNotFoundException → false
  - ReadAllBytesAsync via GetObjectArgs + callback stream, ObjectNotFoundException → null
  - OpenReadAsync: same buffering, ObjectNotFoundException → FileNotFoundException
  - WriteAllBytesAsync via PutObjectArgs with MemoryStream + WithObjectSize
  - OpenWriteAsync returns DeferredWriteStream flushing through WriteAllBytesAsync
  - DeleteAsync: stat-then-remove, returns whether object existed
  - ListAsync: ListObjectsEnumAsync with WithRecursive(true), yields VfsEntry(Key, Size, LastModifiedDateTime)
  - CreateClient mirrors S3StorageService (URI parsing, WithSSL, optional WithRegion)
  - Dispose last, Interlocked-guarded
- RegisterS3FileSystemExtensions: extension(IContainer) block using RegisterDelegate<IVirtualFileSystem> directly (no SquidStd.Vfs dep)
- integration tests: MinioCollection fixture reused, 9 tests all green (Docker available locally)
- slnx + test csproj updated to include new project
- New project SquidStd.Vfs.Database targeting net10.0 with FreeSql 3.5.310
- VfsFileEntity: BaseEntity subclass with Path/Content/Size; unique index on
  Path via FreeSql class-level [Index("uq_vfs_file_path", "Path", true)]
- DatabaseFileSystem: IVirtualFileSystem backed by IDataAccess<VfsFileEntity>;
  query-then-write upsert in WriteAllBytesAsync; StartsWith predicate for
  ListAsync prefix filtering; DeferredWriteStream for OpenWriteAsync
- RegisterDatabaseFileSystemExtensions: extension(IContainer) DryIoc helper
- Integration tests (SQLite, no Docker): write-read round-trip, upsert
  same-path deduplication, ExistsAsync, DeleteAsync, ListAsync with/without
  prefix — all 7 pass
- Added SquidStd.Vfs.Database project reference to SquidStd.Tests
feat(vfs): add VFS decorators and S3/database backends
…S backends

- README: add SquidStd.Tui row under new ### User interface section (after Workers)
- README: update SquidStd.Vfs row to mention composable decorators (ReadOnlyFileSystem,
  ScopedFileSystem, OverlayFileSystem, CachingFileSystem)
- README: add SquidStd.Vfs.S3 and SquidStd.Vfs.Database rows to Storage section
- README: update SquidStd.Crypto row to mention PasswordCipher and Argon2id + AES-256-GCM
- README: extend Overview bullet for Data & storage to mention new VFS backends/decorators;
  add new Terminal UI bullet for SquidStd.Tui
- docs/articles: add tui.md, vfs-s3.md, vfs-database.md include articles
- docs/articles/toc.yml: add SquidStd.Vfs.S3, SquidStd.Vfs.Database after SquidStd.Vfs;
  add SquidStd.Tui before SquidStd.Crypto
- docs/tutorials/tui.md: new tutorial covering CounterViewModel, imperative TuiView<T>,
  declarative TuiComposedView<T> DSL, DI boot, and ViewModel-first navigation
- docs/tutorials/vfs.md: add S3 and database backend sections; add CachingFileSystem,
  ReadOnlyFileSystem, ScopedFileSystem, and OverlayFileSystem decorator examples
- docs/tutorials/crypto.md: add password-based encryption section (PasswordCipher.Encrypt/
  Decrypt, EncryptString/DecryptString, PbkdfCost.Sensitive, PasswordDecryptionException)
- docs/tutorials/toc.yml: add Terminal UI (MVVM) entry
docs: document TUI, password-based encryption, and S3/database VFS backends

@github-advanced-security github-advanced-security AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

@tgiachi
tgiachi merged commit 1869a5f into main Jun 30, 2026
3 checks passed
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