Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
3f43bb9
spike: ComBackendSpike to validate WinGet COM API AOT readiness
harder May 28, 2026
cab1e6d
spike: add Run-AotSpike.ps1 wrapper anchored on $PSScriptRoot
harder May 28, 2026
2aa6e1d
spike: fix AOT InvalidCastException enumerating projected COM collect…
harder May 28, 2026
ae9aba9
spike: AOT recipe is ComInterop + indexing; drop ineffective CsWinRT …
harder May 28, 2026
af467a6
feat: add WinGet COM backend with runtime backend selection
harder May 28, 2026
1413e08
feat: live install progress in the status bar (COM path)
harder May 29, 2026
642835e
feat: cancel in-flight operations with Esc
harder May 29, 2026
79c7987
fix: address Copilot code-review findings on the COM backend
harder May 29, 2026
0301e63
fix: second Copilot review pass on COM backend + backend switch
harder May 29, 2026
b524114
docs: add WINDOWS-TESTING.md — consolidated Windows verification chec…
harder May 29, 2026
a9201c2
feat: install preview dialog + real version picker (COM)
harder May 29, 2026
03bdd41
fix: Copilot review of preview/picker — version fallback + preflight …
harder May 29, 2026
c470695
feat: download-only + advanced install options (COM)
harder May 29, 2026
5747eed
fix: Copilot review of download/advanced-install
harder May 29, 2026
e694110
feat: verify-install action + richer detail panel (COM)
harder May 29, 2026
7417432
fix: Copilot review of verify/richer-detail
harder May 29, 2026
77a17e2
Invalidate detail cache after batch upgrades
harder May 29, 2026
7530f7e
Cancel preflight fetches on navigation
harder May 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 61 additions & 8 deletions Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,15 @@
return;
}

bool useMock = args.Any (a => a is "--mock" or "-m") || !IsWingetAvailable ();

if (useMock && !args.Any (a => a is "--mock" or "-m"))
{
Console.Error.WriteLine ("winget not found on PATH — falling back to mock backend. Run with `winget` available to drive the real CLI.");
}

IBackend backend = useMock ? new MockBackend () : new CliBackend ();
// Backend selection (precedence: --mock > --cli > --com > default):
// --mock / -m the in-memory mock (cross-platform dev/parity)
// --cli the winget.exe CLI parser
// --com the WinGet COM API backend (Windows builds only)
// (default) COM on Windows builds, CLI elsewhere
// These are preferences, not hard guarantees: a requested backend that can't run degrades
// (with a stderr note) — --com on a non-Windows build → CLI, and any CLI path with no winget
// on PATH → mock. Scripts that need a guaranteed backend should check that note.
IBackend backend = SelectBackend (args);

Theme.Register ();

Expand All @@ -84,6 +85,58 @@

return;

static IBackend SelectBackend (string [] args)
{
bool wantMock = args.Any (a => a is "--mock" or "-m");
bool wantCli = args.Any (a => a is "--cli");
bool wantCom = args.Any (a => a is "--com");

if (wantMock)
{
return new MockBackend ();
}

#if WINGET_COM
// Precedence: an explicit --cli always wins over --com (and over the Windows COM default).
// So COM is chosen only when --cli was NOT passed and either --com was, or we're the
// default on Windows.
if (!wantCli && (wantCom || OperatingSystem.IsWindows ()))
{
try
{
return new ComBackend ();
}
catch (Exception ex)
{
// COM server not registered / activation failed — degrade gracefully rather than crash.
Console.Error.WriteLine ($"COM backend unavailable ({ex.Message}); falling back to the CLI backend.");
}
}
#else
if (wantCom)
{
Console.Error.WriteLine ("--com is only available in the Windows build (net10.0-windows…); using the CLI backend instead.");
}
#endif

// CLI path (explicit --cli, or the non-Windows / COM-unavailable default).
if (!IsWingetAvailable ())
{
if (!wantCli)
{
Console.Error.WriteLine ("winget not found on PATH — falling back to mock backend. Run with `winget` available to drive the real CLI.");
}
else
{
Console.Error.WriteLine ("winget not found on PATH — mock backend used despite --cli.");
}

return new MockBackend ();
}

return new CliBackend ();
}

static bool IsWingetAvailable ()
{
try
Expand Down
75 changes: 48 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,19 +103,23 @@ Or right-click the exe → *Properties* → check *Unblock* → *OK*. On the fir

The architecture you build for must match where the binary will run:

| Target Windows machine | Command |
| --------------------------------------------------------------- | ---------------------------------------- |
| Intel / AMD x64 (most Windows PCs) | `dotnet publish -c Release -r win-x64` |
| ARM64 (Surface Pro X, Snapdragon Copilot+ PCs, Windows Dev Kit) | `dotnet publish -c Release -r win-arm64` |
The project multi-targets `net10.0` (cross-platform; mock/CLI backends) and
`net10.0-windows10.0.26100.0` (the Windows deploy target, which adds the COM backend).
Windows release builds must select the Windows TFM with `-f`:

| Target Windows machine | Command |
| --------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| Intel / AMD x64 (most Windows PCs) | `dotnet publish -c Release -f net10.0-windows10.0.26100.0 -r win-x64` |
| ARM64 (Surface Pro X, Snapdragon Copilot+ PCs, Windows Dev Kit) | `dotnet publish -c Release -f net10.0-windows10.0.26100.0 -r win-arm64` |

```powershell
# x64 (Intel/AMD)
dotnet publish -c Release -r win-x64
.\bin\Release\net10.0\win-x64\publish\winget-tui-sharp.exe
dotnet publish -c Release -f net10.0-windows10.0.26100.0 -r win-x64
.\bin\Release\net10.0-windows10.0.26100.0\win-x64\publish\winget-tui-sharp.exe

# arm64
dotnet publish -c Release -r win-arm64
.\bin\Release\net10.0\win-arm64\publish\winget-tui-sharp.exe
dotnet publish -c Release -f net10.0-windows10.0.26100.0 -r win-arm64
.\bin\Release\net10.0-windows10.0.26100.0\win-arm64\publish\winget-tui-sharp.exe
```

**Cross-architecture compile** (`x64 → arm64` or `arm64 → x64`) works on Windows as long
Expand All @@ -126,13 +130,24 @@ Copy `winget-tui-sharp.exe` anywhere, no other files required.

### Dev iteration on any host (including WSL / macOS / Linux)

For iterating on the code, `dotnet run` is faster than re-publishing AOT each time, and unlike the AOT publish it works on any OS - handy for hacking on the UI from WSL. There's no `winget` to invoke on non-Windows hosts, so use `--mock`:
For iterating on the code, `dotnet run` is faster than re-publishing AOT each time, and unlike the AOT publish it works on any OS - handy for hacking on the UI from WSL. Because the project multi-targets, pick the cross-platform TFM with `-f net10.0` off-Windows. There's no `winget` to invoke on non-Windows hosts, so use `--mock`:

```bash
dotnet run # Windows: hits real winget
dotnet run -- --mock # any host: mock backend, useful for UI development
dotnet run -f net10.0 # any host: auto-falls back to mock if winget is absent
dotnet run -f net10.0 -- --mock # any host: force the mock backend (UI development)
```

#### Choosing a backend at runtime

| Flag | Backend | Notes |
| ----------- | ------------- | ---------------------------------------------------------------- |
| `--mock` / `-m` | `MockBackend` | In-memory fixtures; works on any OS. |
| `--cli` | `CliBackend` | Shells out to `winget.exe` and parses its table output. |
| `--com` | `ComBackend` | WinGet **COM API** — structured results, no stdout parsing. Windows build only. |
| _(default)_ | COM on Windows builds, CLI elsewhere | Either degrades to the mock backend if `winget` isn't usable. |

The COM backend talks to the WinGet COM API directly instead of parsing CLI output. Pinning has no COM surface, so pin/unpin/list-pins transparently delegate to the CLI. See [`spikes/ComBackendSpike/SPIKE-RESULTS.md`](spikes/ComBackendSpike/SPIKE-RESULTS.md) for the AOT validation behind it.

### Run the test suite

```bash
Expand Down Expand Up @@ -243,33 +258,38 @@ Mirrors `src/handler.rs` in the upstream:
│ IBackend │
│ Search · ListInstalled · ListUpgrades · Show │
│ Install · Uninstall · Upgrade · Pin · Unpin · ListPins │
└─────────────┬───────────────────────────────────┬───────────┘
▼ ▼
┌──────────────────────────────┐ ┌──────────────────────────┐
│ CliBackend (Windows) │ │ MockBackend (--mock) │
│ ParseTable / ParseShow / │ │ in-memory fixtures so │
│ ParsePins / dedupe / │ │ the UI runs on any host │
│ display-width column slice │ │ for development │
└──────────────┬───────────────┘ └──────────────────────────┘
┌─────────────────────────────────────────┐
│ winget.exe (system, Windows-only) │
└─────────────────────────────────────────┘
└────────┬──────────────────┬───────────────────────┬─────────┘
▼ ▼ ▼
┌─────────────────┐ ┌──────────────────┐ ┌──────────────────────┐
│ ComBackend │ │ CliBackend │ │ MockBackend (--mock)│
│ (--com, Win; │ │ (--cli) │ │ in-memory fixtures │
│ default on Win)│ │ ParseTable / │ │ so the UI runs on │
│ WinGet COM API; │ │ ParseShow / │ │ any host for dev │
│ indexed access; │ │ ParsePins / │ └──────────────────────┘
│ pins → CLI │ │ dedupe │
└───────┬─────────┘ └────────┬─────────┘
▼ ▼
┌─────────────────┐ ┌─────────────────────────────────────────┐
│ WinGet COM │ │ winget.exe (system, Windows-only) │
│ server (Win) │ └─────────────────────────────────────────┘
└─────────────────┘
```

Three layers, top to bottom: **UI** (`App` owns the widgets from `Ui.cs` plus
`DetailPanel`), **state** (`AppState` is the single source of truth for what's filtered
and selected, with generation counters that invalidate stale async responses), and
**backend** (`IBackend` interface, two implementations). Async results from the
backend flow back through `App.Invoke` on the UI thread, where they pass through
the generation guard before mutating `AppState` and triggering a redraw.
**backend** (`IBackend` interface, three implementations selected at runtime — see
[Choosing a backend](#choosing-a-backend-at-runtime)). The `ComBackend` is compiled
only into the Windows TFM. Async results from the backend flow back through
`App.Invoke` on the UI thread, where they pass through the generation guard before
mutating `AppState` and triggering a redraw.

## Project layout

```
winget-tui-sharp/
├── Program.cs # Entry point + winget-detection + --dump diagnostic
├── WingetTuiSharp.csproj # PackageReference on Terminal.Gui; AOT-configured
├── WingetTuiSharp.csproj # Multi-targets net10.0 + net10.0-windows; Terminal.Gui; AOT-configured
├── README.md
├── LICENSE # MIT
├── feature-gaps.md # Terminal.Gui parity findings vs upstream
Expand All @@ -279,6 +299,7 @@ winget-tui-sharp/
│ ├── Models.cs # Package, PackageDetail, enums, OpResult
│ ├── Backend.cs # IBackend interface
│ ├── CliBackend.cs # Shells out to winget; parses table output
│ ├── ComBackend.cs # WinGet COM API backend (Windows TFM only; pins → CLI)
│ ├── MockBackend.cs # Fake packages so the UI runs anywhere
│ ├── AppState.cs # Filters, sort, selection, generation counters
│ ├── Theme.cs # Warm-amber palette + Schemes + pixel-art Logo
Expand Down
Loading
Loading