Skip to content

Commit 09908ff

Browse files
authored
Merge pull request #22 from tgiachi/docs/session-features
docs: document TUI, password-based encryption, and S3/database VFS backends
2 parents 1821c56 + d53cec1 commit 09908ff

9 files changed

Lines changed: 285 additions & 3 deletions

File tree

README.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,14 @@ it bundles the foundations you reach for again and again behind small, well-defi
4444
mailboxes), job system, timer/cron scheduler, metrics and health checks.
4545
- **Messaging & caching** — in-memory, RabbitMQ and AWS SQS/SNS transports; in-memory and Redis caches.
4646
- **Data & storage** — FreeSql data access, binary persistence (snapshot + WAL), local / S3 / MinIO
47-
object storage, and a virtual filesystem (physical / zip / in-memory + an encrypted vault).
47+
object storage, and a virtual filesystem (physical / zip / in-memory + an encrypted vault, plus
48+
S3-compatible and database-backed backends and composable decorators).
4849
- **Search, mail & workers** — Elasticsearch indexing with a constrained LINQ provider, IMAP/POP3
4950
mail polling and an outbound mail queue, and a worker / manager runtime.
5051
- **Networking, scripting & observability** — TCP/UDP servers with a framing/middleware pipeline,
5152
Lua scripting and Scriban templating, and OpenTelemetry tracing + metrics export.
53+
- **Terminal UI** — MVVM for terminal apps via `SquidStd.Tui`: observable ViewModels, a fluent
54+
binder (+ `AutoBind` and a declarative DSL), ViewModel-first navigation, DryIoc wiring.
5255

5356
Everything is modular: take only the packages you need, each behind a clean abstraction with an
5457
in-memory implementation for tests and an external backend for production.
@@ -195,13 +198,15 @@ dotnet new squidstd-manager -n Acme.Manager --messaging inmemory
195198
| `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/) |
196199
| `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/) |
197200
| `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/) |
198-
| `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/) |
201+
| `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/) |
202+
| `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/) |
203+
| `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/) |
199204

200205
### Security — crypto & secrets
201206

202207
| Package | Description | Links |
203208
|---------|-------------|-------|
204-
| `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/) |
209+
| `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/) |
205210
| `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/) |
206211

207212
### Search
@@ -227,6 +232,12 @@ dotnet new squidstd-manager -n Acme.Manager --messaging inmemory
227232
| `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/) |
228233
| `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/) |
229234

235+
### User interface
236+
237+
| Package | Description | Links |
238+
|---------|-------------|-------|
239+
| `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/) |
240+
230241
### Scripting & templating
231242

232243
| Package | Description | Links |

docs/articles/toc.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@
8080
href: vfs-abstractions.md
8181
- name: SquidStd.Vfs
8282
href: vfs.md
83+
- name: SquidStd.Vfs.S3
84+
href: vfs-s3.md
85+
- name: SquidStd.Vfs.Database
86+
href: vfs-database.md
8387
- name: SquidStd.Scripting.Lua
8488
href: scripting-lua.md
8589
- name: SquidStd.Templating
@@ -114,6 +118,8 @@
114118
href: telemetry-opentelemetry.md
115119
- name: SquidStd.Aws.Abstractions
116120
href: aws-abstractions.md
121+
- name: SquidStd.Tui
122+
href: tui.md
117123
- name: SquidStd.Crypto
118124
href: crypto.md
119125
- name: SquidStd.Secrets.Aws

docs/articles/tui.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[!include[](../../src/SquidStd.Tui/README.md)]

docs/articles/vfs-database.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[!include[](../../src/SquidStd.Vfs.Database/README.md)]

docs/articles/vfs-s3.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[!include[](../../src/SquidStd.Vfs.S3/README.md)]

docs/tutorials/crypto.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,37 @@ survives a restart.
4343

4444
[!code-csharp[](../../samples/SquidStd.Samples.Crypto/Program.cs#step-4)]
4545

46+
## Password-based encryption
47+
48+
`PasswordCipher` encrypts a payload directly under a password — no key management required. Argon2id
49+
derives the key, AES-256-GCM seals the data, and the result is a self-describing, versioned envelope
50+
(salt, nonce, tag and KDF cost are embedded). Decryption needs only the password and the blob.
51+
52+
```csharp
53+
using SquidStd.Crypto.Password;
54+
using SquidStd.Crypto.Password.Data;
55+
56+
// Bytes round-trip.
57+
byte[] blob = PasswordCipher.Encrypt(payloadBytes, "correct horse battery staple");
58+
byte[] back = PasswordCipher.Decrypt(blob, "correct horse battery staple");
59+
60+
// Text round-trip — the envelope is base64-encoded, safe to store in config or JSON.
61+
string protectedText = PasswordCipher.EncryptString("a secret", "pw");
62+
string clear = PasswordCipher.DecryptString(protectedText, "pw");
63+
```
64+
65+
The cost of the Argon2id key derivation defaults to `PbkdfCost.Moderate`. Raise it when protecting
66+
long-lived secrets:
67+
68+
```csharp
69+
// PbkdfCost.Sensitive — slower derivation, stronger resistance to offline attacks.
70+
byte[] strong = PasswordCipher.Encrypt(payloadBytes, "pw", PbkdfCost.Sensitive);
71+
```
72+
73+
A wrong password or tampered data raises `PasswordDecryptionException`. Use `PasswordCipher` for
74+
user-supplied passwords; for app-key or KMS encryption use `CryptoUtils` / `ISecretProtector`
75+
instead.
76+
4677
## Run it
4778

4879
```bash

docs/tutorials/toc.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,7 @@
4444
href: vfs.md
4545
- name: Crypto (PGP)
4646
href: crypto.md
47+
- name: Terminal UI (MVVM)
48+
href: tui.md
4749
- name: Secrets (KMS / Secrets Manager)
4850
href: secrets.md

docs/tutorials/tui.md

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# Terminal UI (MVVM)
2+
3+
Build a Counter terminal app with an observable ViewModel, then display it two ways — the
4+
imperative `TuiView<T>` and the declarative `TuiComposedView<T>` DSL.
5+
6+
## What you'll build
7+
8+
A small Counter app where a `CounterViewModel` holds a `Value` property and an `IncrementCommand`.
9+
You will see both ways to build the view:
10+
11+
1. **Imperative**`TuiView<T>`: override `BuildLayout` to add Terminal.Gui widgets, then
12+
override `Bind` to wire them through `ViewBinder`.
13+
2. **Declarative**`TuiComposedView<T>`: override `Compose` and return a `TuiNode` tree built
14+
with the `Ui.*` DSL; bindings and layout are inferred from the node types.
15+
16+
## Prerequisites
17+
18+
- .NET 10 SDK
19+
- `dotnet add package SquidStd.Tui`
20+
21+
## Steps
22+
23+
### 1. Define the ViewModel
24+
25+
Derive from `TuiViewModel` (which extends `ObservableObject` from CommunityToolkit.Mvvm) and mark
26+
observable properties and relay commands with the standard source-generator attributes.
27+
28+
```csharp
29+
using CommunityToolkit.Mvvm.ComponentModel;
30+
using CommunityToolkit.Mvvm.Input;
31+
using SquidStd.Tui.ViewModels;
32+
33+
public sealed partial class CounterViewModel : TuiViewModel
34+
{
35+
[ObservableProperty]
36+
private string _title = "Counter";
37+
38+
[ObservableProperty]
39+
private string _value = "0";
40+
41+
[RelayCommand]
42+
private void Increment() => Value = (int.Parse(Value) + 1).ToString();
43+
}
44+
```
45+
46+
### 2. Imperative view — `TuiView<T>`
47+
48+
Override `BuildLayout` to create and add Terminal.Gui widgets, then override `Bind` to wire them
49+
through `ViewBinder`. The binder tracks property-changed notifications and updates widgets without
50+
polling.
51+
52+
```csharp
53+
using Terminal.Gui;
54+
using SquidStd.Tui.Views;
55+
using SquidStd.Tui.Binders;
56+
57+
public sealed class CounterView : TuiView<CounterViewModel>
58+
{
59+
private Label _value = null!;
60+
private Button _inc = null!;
61+
62+
protected override void BuildLayout()
63+
{
64+
_value = new Label { X = 1, Y = 1 };
65+
_inc = new Button { X = 1, Y = 3, Text = "+1" };
66+
Add(_value, _inc);
67+
}
68+
69+
protected override void Bind(ViewBinder b)
70+
{
71+
b.OneWayTitle(ViewModel, x => x.Title, this);
72+
b.OneWayText(ViewModel, x => x.Value, _value);
73+
b.Command(_inc, ViewModel.IncrementCommand);
74+
}
75+
}
76+
```
77+
78+
### 3. Declarative view — `TuiComposedView<T>`
79+
80+
Instead of `BuildLayout` + `Bind`, derive from `TuiComposedView<T>` and return a node tree from
81+
`Compose`. The framework infers binding direction from the node type (Label → one-way, TextField →
82+
two-way, Button → command) and applies layout automatically.
83+
84+
```csharp
85+
using SquidStd.Tui.Views;
86+
using SquidStd.Tui.Dsl;
87+
88+
public sealed class CounterView : TuiComposedView<CounterViewModel>
89+
{
90+
protected override TuiNode<CounterViewModel> Compose() =>
91+
Ui.VStack(
92+
Ui.Label(x => x.Title),
93+
Ui.Label(x => x.Value),
94+
Ui.Button("+1", x => x.IncrementCommand));
95+
}
96+
```
97+
98+
`Ui.VStack` / `Ui.HStack` stack children vertically or horizontally and auto-assign `Y`/`X`
99+
offsets. `Ui.TextField(x => x.Name)` is two-way by default; pass `BindMode.OneWay` to override.
100+
101+
### 4. Register and run
102+
103+
`RegisterTui()` adds the core TUI services and `TuiApplicationHost` to the DryIoc container.
104+
`RegisterView<TView, TViewModel>()` registers the view for ViewModel-first resolution.
105+
`TuiApplicationHost.RunAsync<TViewModel>()` resolves the root ViewModel, shows its view, and starts
106+
the Terminal.Gui event loop.
107+
108+
```csharp
109+
using DryIoc;
110+
using SquidStd.Tui.Extensions;
111+
using SquidStd.Tui.Hosts;
112+
113+
var container = new Container().RegisterTui();
114+
container.RegisterView<CounterView, CounterViewModel>();
115+
116+
await container.Resolve<TuiApplicationHost>().RunAsync<CounterViewModel>();
117+
```
118+
119+
### 5. ViewModel-first navigation
120+
121+
Push a new ViewModel onto the navigation stack from inside an existing ViewModel via the injected
122+
`ITuiNavigator`:
123+
124+
```csharp
125+
// inside CounterViewModel
126+
await Navigator.NavigateToAsync<DetailViewModel>();
127+
await Navigator.BackAsync();
128+
```
129+
130+
The navigator resolves the view that was registered for `DetailViewModel` and shows it; `BackAsync`
131+
pops the stack and restores the previous view.
132+
133+
## Next steps
134+
135+
- [SquidStd.Tui reference](../articles/tui.md)

docs/tutorials/vfs.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,98 @@ zip — as a lockable singleton.
4242

4343
[!code-csharp[](../../samples/SquidStd.Samples.Vfs/Program.cs#step-3)]
4444

45+
## Alternative backends
46+
47+
### S3-compatible storage
48+
49+
Swap the in-memory backend for an S3-compatible store (AWS S3, MinIO, Cloudflare R2, Backblaze B2)
50+
by installing `SquidStd.Vfs.S3` and replacing the `RegisterVfs` call:
51+
52+
```bash
53+
dotnet add package SquidStd.Vfs.S3
54+
```
55+
56+
```csharp
57+
using SquidStd.Vfs.S3.Extensions;
58+
59+
container.RegisterS3FileSystem(o =>
60+
{
61+
o.Bucket = "app-data";
62+
o.Aws.ServiceUrl = "https://s3.amazonaws.com"; // or MinIO/R2/B2 endpoint
63+
o.Aws.AccessKey = "...";
64+
o.Aws.SecretKey = "...";
65+
});
66+
67+
// IVirtualFileSystem is now backed by S3 — the rest of your code is unchanged.
68+
var fs = container.Resolve<IVirtualFileSystem>();
69+
await fs.WriteAllBytesAsync("reports/2026.json", payload);
70+
```
71+
72+
For native AWS with the default credential chain, omit `AccessKey`/`SecretKey` and set only
73+
`o.Aws.Region`.
74+
75+
### Database-backed storage
76+
77+
Store files as rows in a relational database (SQLite, MySQL, PostgreSQL, …) with
78+
`SquidStd.Vfs.Database`. Register `SquidStd.Database` first, then add the VFS backend:
79+
80+
```bash
81+
dotnet add package SquidStd.Vfs.Database
82+
```
83+
84+
```csharp
85+
using SquidStd.Vfs.Database.Extensions;
86+
87+
// SquidStd.Database must already be registered on the container.
88+
container.RegisterDatabaseFileSystem();
89+
90+
var fs = container.Resolve<IVirtualFileSystem>();
91+
await fs.WriteAllBytesAsync("config/settings.json", payload);
92+
```
93+
94+
FreeSql creates the `VfsFileEntity` table automatically on first access. This backend is
95+
well-suited to single-process scenarios or where last-write-wins is acceptable.
96+
97+
## Composable decorators
98+
99+
Decorators wrap any `IVirtualFileSystem` to add behaviour without touching the backend. Construct
100+
them directly and pass the result to `RegisterVfs`:
101+
102+
### Offline-resilient reads with `CachingFileSystem`
103+
104+
Wrap a remote backend (S3, database) with a local cache to keep reads working when the remote is
105+
temporarily unreachable:
106+
107+
```csharp
108+
using SquidStd.Vfs.Services;
109+
using SquidStd.Vfs.S3.Services;
110+
111+
var s3 = new S3FileSystem(s3Options);
112+
113+
container.RegisterVfs(_ => new CachingFileSystem(
114+
remote: s3,
115+
cache: new PhysicalFileSystem("/var/cache/app")));
116+
```
117+
118+
Reads prefer the remote and refresh the cache on success; on a transport failure they fall back to
119+
the stale cached copy. Writes are write-through (remote then cache) and fail when the remote is
120+
unreachable.
121+
122+
### Other decorators
123+
124+
```csharp
125+
// Reject all writes — safe read-only access to a shared backend.
126+
container.RegisterVfs(_ => new ReadOnlyFileSystem(new PhysicalFileSystem("/shared/data")));
127+
128+
// Chroot to a subdirectory — all paths are resolved relative to the prefix.
129+
container.RegisterVfs(_ => new ScopedFileSystem(new PhysicalFileSystem("/var/lib/app"), "tenant-1"));
130+
131+
// Overlay — reads overlay-first then fall back to base; writes go to the overlay only.
132+
container.RegisterVfs(_ => new OverlayFileSystem(
133+
baseFileSystem: new PhysicalFileSystem("/defaults"),
134+
overlay: new InMemoryFileSystem()));
135+
```
136+
45137
## Run it
46138

47139
```bash
@@ -51,3 +143,5 @@ dotnet run --project samples/SquidStd.Samples.Vfs
51143
## Next steps
52144

53145
- [SquidStd.Vfs reference](../articles/vfs.md)
146+
- [SquidStd.Vfs.S3 reference](../articles/vfs-s3.md)
147+
- [SquidStd.Vfs.Database reference](../articles/vfs-database.md)

0 commit comments

Comments
 (0)