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