diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 0fce33c..9d77c0d 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -2,8 +2,8 @@ name: Release
# Tag-driven release. To cut a release:
#
-# git tag v0.2.0
-# git push origin v0.2.0
+# git tag v0.2.1
+# git push origin v0.2.1
#
# Builds win-x64 and win-arm64 AOT executables, packages them with the README and
# LICENSE into zips, computes SHA-256 checksums, and publishes a GitHub Release with
diff --git a/WinGetScout.csproj b/WinGetScout.csproj
index 3cc6496..6e7a721 100644
--- a/WinGetScout.csproj
+++ b/WinGetScout.csproj
@@ -20,7 +20,7 @@
wingetscout
Scout for WinGet
Scout for WinGet
- 0.2.0
+ 0.2.1
Scout for WinGet is a terminal app for finding, installing, and updating Windows packages.
true
true
diff --git a/packaging/WINGET-SUBMISSION.md b/packaging/WINGET-SUBMISSION.md
index ddbc83d..c508b61 100644
--- a/packaging/WINGET-SUBMISSION.md
+++ b/packaging/WINGET-SUBMISSION.md
@@ -8,7 +8,7 @@ The installer manifest has `InstallerType: zip`, `NestedInstallerType: portable`
2. Download that artifact from the successful release workflow run. The files are under `manifests/h/Harder/WinGetScout//`. To regenerate directly from the published ZIPs and validate in one command on Windows, run:
```powershell
- pwsh ./packaging/prepare-winget-submission.ps1 -Version 0.2.0
+ pwsh ./packaging/prepare-winget-submission.ps1 -Version 0.2.1
```
The helper needs `gh`, `winget`, and a GitHub login with access to the public release. It downloads the exact published assets and uses [new-winget-manifest.ps1](new-winget-manifest.ps1) to check the archive layout and compute SHA-256 values. The release workflow also verifies its published ZIPs are byte-for-byte identical to its build artifacts. Before submitting, inspect the generated URLs and hashes against the GitHub Release.
diff --git a/packaging/build-msix.ps1 b/packaging/build-msix.ps1
index e426ff8..0f5a9ac 100644
--- a/packaging/build-msix.ps1
+++ b/packaging/build-msix.ps1
@@ -16,12 +16,12 @@
.EXAMPLE
# CI: pack only (sign separately with Azure Artifact Signing, see code-signing.md):
- pwsh ./packaging/build-msix.ps1 -Arch x64 -Version 0.2.0.0
+ pwsh ./packaging/build-msix.ps1 -Arch x64 -Version 0.2.1.0
#>
[CmdletBinding()]
param(
[ValidateSet('x64', 'arm64')] [string]$Arch = 'arm64',
- [string]$Version = '0.2.0.0',
+ [string]$Version = '0.2.1.0',
[string]$Publisher = 'CN=wingetscout (Dev)',
[string]$CertPath,
[string]$CertPassword = 'spike',
diff --git a/src/App.cs b/src/App.cs
index d137a3b..c7c8c46 100644
--- a/src/App.cs
+++ b/src/App.cs
@@ -11,6 +11,8 @@ public sealed partial class App : Window
{
/// One navigation row and one contextual row before the main content.
private const int HeaderHeight = 2;
+ private const int ScoutWidth = 17;
+ private const int ScoutHeight = 4;
// Debounce before an uncached detail fetch fires. Scrolling the list changes the selection
// rapidly; without this, every row the cursor passes over issues a full backend detail
@@ -28,6 +30,7 @@ public sealed partial class App : Window
private readonly FrameView _listFrame;
private readonly SortableTableView _packageTable;
private readonly Label _mainLoadingLabel;
+ private readonly Label _scoutMascot;
private readonly DetailPanel _detailPanel;
private readonly StatusBar _statusBar;
private readonly TerminalSizeGuardView _sizeGuard;
@@ -148,11 +151,27 @@ public App (IBackend backend, TimeSpan? smokeDelay = null)
Visible = false
};
+ _scoutMascot = new ()
+ {
+ X = Pos.AnchorEnd (ScoutWidth + 1),
+ Y = Pos.AnchorEnd (ScoutHeight + 1),
+ Width = ScoutWidth,
+ Height = ScoutHeight,
+ Text = " / \\__\n ( @\\___\n / O[ ]\n / (_____/",
+ SchemeName = Theme.SurfaceSchemeName,
+ CanFocus = false,
+ Visible = false
+ };
+
// The list frame stays visible while the table is replaced or hidden for loading. Its
// interior width is the stable column budget; the table's own viewport can change when
// selection or scrolling updates its content.
- _listFrame.ViewportChanged += (_, _) => ApplyColumnWidths ();
- _listFrame.Add (_packageTable, _mainLoadingLabel);
+ _listFrame.ViewportChanged += (_, _) =>
+ {
+ ApplyColumnWidths ();
+ UpdateScoutVisibility ();
+ };
+ _listFrame.Add (_packageTable, _mainLoadingLabel, _scoutMascot);
_detailPanel = new ()
{
@@ -839,6 +858,7 @@ private void ShowMainLoading (AppMode mode)
_mainLoadingMode = mode;
_mainLoadingLabel.Text = MainLoadingText (mode, _statusBar.Tick);
_mainLoadingLabel.Visible = true;
+ UpdateScoutVisibility ();
_listFrame.Title = $" {mode} (loading…) ";
_detailPanel.SetDetail (null, true);
}
@@ -848,6 +868,7 @@ private void HideMainLoading ()
_mainLoadingMode = null;
_mainLoadingLabel.Visible = false;
_packageTable.Visible = true;
+ UpdateScoutVisibility ();
}
private void ShowMainLoadError (AppMode mode)
@@ -856,6 +877,7 @@ private void ShowMainLoadError (AppMode mode)
_mainLoadingLabel.Text = $"\nCould not load {MainLoadingName (mode)}. See status bar.";
_mainLoadingLabel.Visible = true;
_packageTable.Visible = false;
+ UpdateScoutVisibility ();
_detailPanel.SetDetail (null, false);
_listFrame.Title = $" {mode} (error) ";
}
@@ -879,6 +901,7 @@ private void RefreshTable ()
});
RefreshStatusBar ();
+ UpdateScoutVisibility ();
return;
}
@@ -934,9 +957,22 @@ private void RefreshTable ()
_packageTable.Table = marked;
ApplyColumnStyles (marked);
+ UpdateScoutVisibility ();
OnSelectedRowChanged ();
}
+ private void UpdateScoutVisibility ()
+ {
+ // The table uses two rows for its header and underline. Keep two more blank rows
+ // between the last package and the drawing, including when the pane is resized.
+ int scoutTop = _listFrame.Viewport.Height - ScoutHeight - 1;
+ _scoutMascot.Visible = _packageTable.Visible
+ && !_mainLoadingLabel.Visible
+ && string.IsNullOrEmpty (_state.ViewError)
+ && _listFrame.Viewport.Width >= ScoutWidth + 2
+ && scoutTop >= _state.Filtered.Count + 4;
+ }
+
private void UpdateListTitle ()
{
string title = _state.Mode switch
diff --git a/tests/AppBehaviorTests.cs b/tests/AppBehaviorTests.cs
index 20665d7..632e766 100644
--- a/tests/AppBehaviorTests.cs
+++ b/tests/AppBehaviorTests.cs
@@ -122,6 +122,55 @@ public void App_WideWindow_UsesSkillViewStyleCompactHeader ()
Assert.Equal (2, listFrame.Frame.Y);
}
+ [Theory]
+ [InlineData (AppMode.Installed)]
+ [InlineData (AppMode.Upgrades)]
+ [InlineData (AppMode.Search)]
+ public void App_ScoutAppearsBelowShortListsOnEveryTab (AppMode mode)
+ {
+ App app = new (new MockBackend ()) { Frame = new (0, 0, 100, 30) };
+ AppState state = GetPrivateField (app, "_state");
+ FrameView list = GetPrivateField (app, "_listFrame");
+ Label scout = GetPrivateField