diff --git a/README.md b/README.md index ff64646b..a665af5b 100644 --- a/README.md +++ b/README.md @@ -44,11 +44,14 @@ it bundles the foundations you reach for again and again behind small, well-defi mailboxes), job system, timer/cron scheduler, metrics and health checks. - **Messaging & caching** — in-memory, RabbitMQ and AWS SQS/SNS transports; in-memory and Redis caches. - **Data & storage** — FreeSql data access, binary persistence (snapshot + WAL), local / S3 / MinIO - object storage, and a virtual filesystem (physical / zip / in-memory + an encrypted vault). + object storage, and a virtual filesystem (physical / zip / in-memory + an encrypted vault, plus + S3-compatible and database-backed backends and composable decorators). - **Search, mail & workers** — Elasticsearch indexing with a constrained LINQ provider, IMAP/POP3 mail polling and an outbound mail queue, and a worker / manager runtime. - **Networking, scripting & observability** — TCP/UDP servers with a framing/middleware pipeline, Lua scripting and Scriban templating, and OpenTelemetry tracing + metrics export. +- **Terminal UI** — MVVM for terminal apps via `SquidStd.Tui`: observable ViewModels, a fluent + binder (+ `AutoBind` and a declarative DSL), ViewModel-first navigation, DryIoc wiring. Everything is modular: take only the packages you need, each behind a clean abstraction with an in-memory implementation for tests and an external backend for production. @@ -195,13 +198,15 @@ dotnet new squidstd-manager -n Acme.Manager --messaging inmemory | `SquidStd.Storage` | Local file storage backend (`AddFileStorage`). | [![readme](https://img.shields.io/badge/readme-1390A3.svg)](src/SquidStd.Storage/README.md) · [![NuGet](https://img.shields.io/nuget/v/SquidStd.Storage.svg)](https://www.nuget.org/packages/SquidStd.Storage/) | | `SquidStd.Storage.S3` | S3/MinIO storage backend (`AddS3Storage`). | [![readme](https://img.shields.io/badge/readme-1390A3.svg)](src/SquidStd.Storage.S3/README.md) · [![NuGet](https://img.shields.io/nuget/v/SquidStd.Storage.S3.svg)](https://www.nuget.org/packages/SquidStd.Storage.S3/) | | `SquidStd.Vfs.Abstractions` | Virtual filesystem contracts (`IVirtualFileSystem`, `ILockableFileSystem`, `VfsPath`). | [![readme](https://img.shields.io/badge/readme-1390A3.svg)](src/SquidStd.Vfs.Abstractions/README.md) · [![NuGet](https://img.shields.io/nuget/v/SquidStd.Vfs.Abstractions.svg)](https://www.nuget.org/packages/SquidStd.Vfs.Abstractions/) | -| `SquidStd.Vfs` | Virtual filesystem providers — physical, in-memory, and zip — plus `VfsDirectories`, a VFS-backed `DirectoriesConfig`. | [![readme](https://img.shields.io/badge/readme-1390A3.svg)](src/SquidStd.Vfs/README.md) · [![NuGet](https://img.shields.io/nuget/v/SquidStd.Vfs.svg)](https://www.nuget.org/packages/SquidStd.Vfs/) | +| `SquidStd.Vfs` | Virtual filesystem providers — physical, in-memory, and zip — plus composable decorators (`ReadOnlyFileSystem`, `ScopedFileSystem`, `OverlayFileSystem`, `CachingFileSystem`) and `VfsDirectories`. | [![readme](https://img.shields.io/badge/readme-1390A3.svg)](src/SquidStd.Vfs/README.md) · [![NuGet](https://img.shields.io/nuget/v/SquidStd.Vfs.svg)](https://www.nuget.org/packages/SquidStd.Vfs/) | +| `SquidStd.Vfs.S3` | S3-compatible VFS backend — AWS S3, MinIO, Cloudflare R2, Backblaze B2 — via the MinIO SDK (`RegisterS3FileSystem`). | [![readme](https://img.shields.io/badge/readme-1390A3.svg)](src/SquidStd.Vfs.S3/README.md) · [![NuGet](https://img.shields.io/nuget/v/SquidStd.Vfs.S3.svg)](https://www.nuget.org/packages/SquidStd.Vfs.S3/) | +| `SquidStd.Vfs.Database` | Database-backed VFS storing files as rows via SquidStd.Database / FreeSql (`RegisterDatabaseFileSystem`). | [![readme](https://img.shields.io/badge/readme-1390A3.svg)](src/SquidStd.Vfs.Database/README.md) · [![NuGet](https://img.shields.io/nuget/v/SquidStd.Vfs.Database.svg)](https://www.nuget.org/packages/SquidStd.Vfs.Database/) | ### Security — crypto & secrets | Package | Description | Links | |---------|-------------|-------| -| `SquidStd.Crypto` | OpenPGP key management/operations over an indexed keyring (`SquidStd.Crypto.Pgp`, `RegisterPgp`) plus the encrypted VFS vault decorator (Argon2id + per-entry AES-GCM). | [![readme](https://img.shields.io/badge/readme-1390A3.svg)](src/SquidStd.Crypto/README.md) · [![NuGet](https://img.shields.io/nuget/v/SquidStd.Crypto.svg)](https://www.nuget.org/packages/SquidStd.Crypto/) | +| `SquidStd.Crypto` | OpenPGP key management/operations over an indexed keyring (`SquidStd.Crypto.Pgp`, `RegisterPgp`), password-based encryption (`PasswordCipher`, Argon2id + AES-256-GCM with a self-describing envelope), and the encrypted VFS vault decorator. | [![readme](https://img.shields.io/badge/readme-1390A3.svg)](src/SquidStd.Crypto/README.md) · [![NuGet](https://img.shields.io/nuget/v/SquidStd.Crypto.svg)](https://www.nuget.org/packages/SquidStd.Crypto/) | | `SquidStd.Secrets.Aws` | AWS adapters for the secret seams — KMS envelope `ISecretProtector` and Secrets Manager `ISecretStore`. | [![readme](https://img.shields.io/badge/readme-1390A3.svg)](src/SquidStd.Secrets.Aws/README.md) · [![NuGet](https://img.shields.io/nuget/v/SquidStd.Secrets.Aws.svg)](https://www.nuget.org/packages/SquidStd.Secrets.Aws/) | ### Search @@ -227,6 +232,12 @@ dotnet new squidstd-manager -n Acme.Manager --messaging inmemory | `SquidStd.Workers` | Worker runtime: consume jobs, dispatch to `IJobHandler`s, publish heartbeats (`AddWorkers`). | [![readme](https://img.shields.io/badge/readme-1390A3.svg)](src/SquidStd.Workers/README.md) · [![NuGet](https://img.shields.io/nuget/v/SquidStd.Workers.svg)](https://www.nuget.org/packages/SquidStd.Workers/) | | `SquidStd.Workers.Manager` | Job enqueue, heartbeat registry, offline sweep, opt-in ASP.NET endpoints (`AddWorkerManager`). | [![readme](https://img.shields.io/badge/readme-1390A3.svg)](src/SquidStd.Workers.Manager/README.md) · [![NuGet](https://img.shields.io/nuget/v/SquidStd.Workers.Manager.svg)](https://www.nuget.org/packages/SquidStd.Workers.Manager/) | +### User interface + +| Package | Description | Links | +|---------|-------------|-------| +| `SquidStd.Tui` | MVVM for terminal apps on Terminal.Gui v2 — observable ViewModels, a fluent binder (+ `AutoBind` and a declarative DSL), ViewModel-first navigation, DryIoc wiring. | [![readme](https://img.shields.io/badge/readme-1390A3.svg)](src/SquidStd.Tui/README.md) · [![NuGet](https://img.shields.io/nuget/v/SquidStd.Tui.svg)](https://www.nuget.org/packages/SquidStd.Tui/) | + ### Scripting & templating | Package | Description | Links | diff --git a/docs/articles/toc.yml b/docs/articles/toc.yml index cc2d4e4e..144f2b18 100644 --- a/docs/articles/toc.yml +++ b/docs/articles/toc.yml @@ -80,6 +80,10 @@ href: vfs-abstractions.md - name: SquidStd.Vfs href: vfs.md +- name: SquidStd.Vfs.S3 + href: vfs-s3.md +- name: SquidStd.Vfs.Database + href: vfs-database.md - name: SquidStd.Scripting.Lua href: scripting-lua.md - name: SquidStd.Templating @@ -114,6 +118,8 @@ href: telemetry-opentelemetry.md - name: SquidStd.Aws.Abstractions href: aws-abstractions.md +- name: SquidStd.Tui + href: tui.md - name: SquidStd.Crypto href: crypto.md - name: SquidStd.Secrets.Aws diff --git a/docs/articles/tui.md b/docs/articles/tui.md new file mode 100644 index 00000000..9cb161da --- /dev/null +++ b/docs/articles/tui.md @@ -0,0 +1 @@ +[!include[](../../src/SquidStd.Tui/README.md)] diff --git a/docs/articles/vfs-database.md b/docs/articles/vfs-database.md new file mode 100644 index 00000000..d91f5b38 --- /dev/null +++ b/docs/articles/vfs-database.md @@ -0,0 +1 @@ +[!include[](../../src/SquidStd.Vfs.Database/README.md)] diff --git a/docs/articles/vfs-s3.md b/docs/articles/vfs-s3.md new file mode 100644 index 00000000..35b71081 --- /dev/null +++ b/docs/articles/vfs-s3.md @@ -0,0 +1 @@ +[!include[](../../src/SquidStd.Vfs.S3/README.md)] diff --git a/docs/tutorials/crypto.md b/docs/tutorials/crypto.md index 95070492..6e8f9b65 100644 --- a/docs/tutorials/crypto.md +++ b/docs/tutorials/crypto.md @@ -43,6 +43,37 @@ survives a restart. [!code-csharp[](../../samples/SquidStd.Samples.Crypto/Program.cs#step-4)] +## Password-based encryption + +`PasswordCipher` encrypts a payload directly under a password — no key management required. Argon2id +derives the key, AES-256-GCM seals the data, and the result is a self-describing, versioned envelope +(salt, nonce, tag and KDF cost are embedded). Decryption needs only the password and the blob. + +```csharp +using SquidStd.Crypto.Password; +using SquidStd.Crypto.Password.Data; + +// Bytes round-trip. +byte[] blob = PasswordCipher.Encrypt(payloadBytes, "correct horse battery staple"); +byte[] back = PasswordCipher.Decrypt(blob, "correct horse battery staple"); + +// Text round-trip — the envelope is base64-encoded, safe to store in config or JSON. +string protectedText = PasswordCipher.EncryptString("a secret", "pw"); +string clear = PasswordCipher.DecryptString(protectedText, "pw"); +``` + +The cost of the Argon2id key derivation defaults to `PbkdfCost.Moderate`. Raise it when protecting +long-lived secrets: + +```csharp +// PbkdfCost.Sensitive — slower derivation, stronger resistance to offline attacks. +byte[] strong = PasswordCipher.Encrypt(payloadBytes, "pw", PbkdfCost.Sensitive); +``` + +A wrong password or tampered data raises `PasswordDecryptionException`. Use `PasswordCipher` for +user-supplied passwords; for app-key or KMS encryption use `CryptoUtils` / `ISecretProtector` +instead. + ## Run it ```bash diff --git a/docs/tutorials/toc.yml b/docs/tutorials/toc.yml index 2d88e934..aa2de778 100644 --- a/docs/tutorials/toc.yml +++ b/docs/tutorials/toc.yml @@ -44,5 +44,7 @@ href: vfs.md - name: Crypto (PGP) href: crypto.md +- name: Terminal UI (MVVM) + href: tui.md - name: Secrets (KMS / Secrets Manager) href: secrets.md diff --git a/docs/tutorials/tui.md b/docs/tutorials/tui.md new file mode 100644 index 00000000..cecafc80 --- /dev/null +++ b/docs/tutorials/tui.md @@ -0,0 +1,135 @@ +# Terminal UI (MVVM) + +Build a Counter terminal app with an observable ViewModel, then display it two ways — the +imperative `TuiView` and the declarative `TuiComposedView` DSL. + +## What you'll build + +A small Counter app where a `CounterViewModel` holds a `Value` property and an `IncrementCommand`. +You will see both ways to build the view: + +1. **Imperative** — `TuiView`: override `BuildLayout` to add Terminal.Gui widgets, then + override `Bind` to wire them through `ViewBinder`. +2. **Declarative** — `TuiComposedView`: override `Compose` and return a `TuiNode` tree built + with the `Ui.*` DSL; bindings and layout are inferred from the node types. + +## Prerequisites + +- .NET 10 SDK +- `dotnet add package SquidStd.Tui` + +## Steps + +### 1. Define the ViewModel + +Derive from `TuiViewModel` (which extends `ObservableObject` from CommunityToolkit.Mvvm) and mark +observable properties and relay commands with the standard source-generator attributes. + +```csharp +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using SquidStd.Tui.ViewModels; + +public sealed partial class CounterViewModel : TuiViewModel +{ + [ObservableProperty] + private string _title = "Counter"; + + [ObservableProperty] + private string _value = "0"; + + [RelayCommand] + private void Increment() => Value = (int.Parse(Value) + 1).ToString(); +} +``` + +### 2. Imperative view — `TuiView` + +Override `BuildLayout` to create and add Terminal.Gui widgets, then override `Bind` to wire them +through `ViewBinder`. The binder tracks property-changed notifications and updates widgets without +polling. + +```csharp +using Terminal.Gui; +using SquidStd.Tui.Views; +using SquidStd.Tui.Binders; + +public sealed class CounterView : TuiView +{ + private Label _value = null!; + private Button _inc = null!; + + protected override void BuildLayout() + { + _value = new Label { X = 1, Y = 1 }; + _inc = new Button { X = 1, Y = 3, Text = "+1" }; + Add(_value, _inc); + } + + protected override void Bind(ViewBinder b) + { + b.OneWayTitle(ViewModel, x => x.Title, this); + b.OneWayText(ViewModel, x => x.Value, _value); + b.Command(_inc, ViewModel.IncrementCommand); + } +} +``` + +### 3. Declarative view — `TuiComposedView` + +Instead of `BuildLayout` + `Bind`, derive from `TuiComposedView` and return a node tree from +`Compose`. The framework infers binding direction from the node type (Label → one-way, TextField → +two-way, Button → command) and applies layout automatically. + +```csharp +using SquidStd.Tui.Views; +using SquidStd.Tui.Dsl; + +public sealed class CounterView : TuiComposedView +{ + protected override TuiNode Compose() => + Ui.VStack( + Ui.Label(x => x.Title), + Ui.Label(x => x.Value), + Ui.Button("+1", x => x.IncrementCommand)); +} +``` + +`Ui.VStack` / `Ui.HStack` stack children vertically or horizontally and auto-assign `Y`/`X` +offsets. `Ui.TextField(x => x.Name)` is two-way by default; pass `BindMode.OneWay` to override. + +### 4. Register and run + +`RegisterTui()` adds the core TUI services and `TuiApplicationHost` to the DryIoc container. +`RegisterView()` registers the view for ViewModel-first resolution. +`TuiApplicationHost.RunAsync()` resolves the root ViewModel, shows its view, and starts +the Terminal.Gui event loop. + +```csharp +using DryIoc; +using SquidStd.Tui.Extensions; +using SquidStd.Tui.Hosts; + +var container = new Container().RegisterTui(); +container.RegisterView(); + +await container.Resolve().RunAsync(); +``` + +### 5. ViewModel-first navigation + +Push a new ViewModel onto the navigation stack from inside an existing ViewModel via the injected +`ITuiNavigator`: + +```csharp +// inside CounterViewModel +await Navigator.NavigateToAsync(); +await Navigator.BackAsync(); +``` + +The navigator resolves the view that was registered for `DetailViewModel` and shows it; `BackAsync` +pops the stack and restores the previous view. + +## Next steps + +- [SquidStd.Tui reference](../articles/tui.md) diff --git a/docs/tutorials/vfs.md b/docs/tutorials/vfs.md index 92082caa..769e599d 100644 --- a/docs/tutorials/vfs.md +++ b/docs/tutorials/vfs.md @@ -42,6 +42,98 @@ zip — as a lockable singleton. [!code-csharp[](../../samples/SquidStd.Samples.Vfs/Program.cs#step-3)] +## Alternative backends + +### S3-compatible storage + +Swap the in-memory backend for an S3-compatible store (AWS S3, MinIO, Cloudflare R2, Backblaze B2) +by installing `SquidStd.Vfs.S3` and replacing the `RegisterVfs` call: + +```bash +dotnet add package SquidStd.Vfs.S3 +``` + +```csharp +using SquidStd.Vfs.S3.Extensions; + +container.RegisterS3FileSystem(o => +{ + o.Bucket = "app-data"; + o.Aws.ServiceUrl = "https://s3.amazonaws.com"; // or MinIO/R2/B2 endpoint + o.Aws.AccessKey = "..."; + o.Aws.SecretKey = "..."; +}); + +// IVirtualFileSystem is now backed by S3 — the rest of your code is unchanged. +var fs = container.Resolve(); +await fs.WriteAllBytesAsync("reports/2026.json", payload); +``` + +For native AWS with the default credential chain, omit `AccessKey`/`SecretKey` and set only +`o.Aws.Region`. + +### Database-backed storage + +Store files as rows in a relational database (SQLite, MySQL, PostgreSQL, …) with +`SquidStd.Vfs.Database`. Register `SquidStd.Database` first, then add the VFS backend: + +```bash +dotnet add package SquidStd.Vfs.Database +``` + +```csharp +using SquidStd.Vfs.Database.Extensions; + +// SquidStd.Database must already be registered on the container. +container.RegisterDatabaseFileSystem(); + +var fs = container.Resolve(); +await fs.WriteAllBytesAsync("config/settings.json", payload); +``` + +FreeSql creates the `VfsFileEntity` table automatically on first access. This backend is +well-suited to single-process scenarios or where last-write-wins is acceptable. + +## Composable decorators + +Decorators wrap any `IVirtualFileSystem` to add behaviour without touching the backend. Construct +them directly and pass the result to `RegisterVfs`: + +### Offline-resilient reads with `CachingFileSystem` + +Wrap a remote backend (S3, database) with a local cache to keep reads working when the remote is +temporarily unreachable: + +```csharp +using SquidStd.Vfs.Services; +using SquidStd.Vfs.S3.Services; + +var s3 = new S3FileSystem(s3Options); + +container.RegisterVfs(_ => new CachingFileSystem( + remote: s3, + cache: new PhysicalFileSystem("/var/cache/app"))); +``` + +Reads prefer the remote and refresh the cache on success; on a transport failure they fall back to +the stale cached copy. Writes are write-through (remote then cache) and fail when the remote is +unreachable. + +### Other decorators + +```csharp +// Reject all writes — safe read-only access to a shared backend. +container.RegisterVfs(_ => new ReadOnlyFileSystem(new PhysicalFileSystem("/shared/data"))); + +// Chroot to a subdirectory — all paths are resolved relative to the prefix. +container.RegisterVfs(_ => new ScopedFileSystem(new PhysicalFileSystem("/var/lib/app"), "tenant-1")); + +// Overlay — reads overlay-first then fall back to base; writes go to the overlay only. +container.RegisterVfs(_ => new OverlayFileSystem( + baseFileSystem: new PhysicalFileSystem("/defaults"), + overlay: new InMemoryFileSystem())); +``` + ## Run it ```bash @@ -51,3 +143,5 @@ dotnet run --project samples/SquidStd.Samples.Vfs ## Next steps - [SquidStd.Vfs reference](../articles/vfs.md) +- [SquidStd.Vfs.S3 reference](../articles/vfs-s3.md) +- [SquidStd.Vfs.Database reference](../articles/vfs-database.md)