Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion WinGetScout.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
<AssemblyName>wingetscout</AssemblyName>
<Product>Scout for WinGet</Product>
<Title>Scout for WinGet</Title>
<Version>0.2.0</Version>
<Version>0.2.1</Version>
<Description>Scout for WinGet is a terminal app for finding, installing, and updating Windows packages.</Description>
<PublishAot>true</PublishAot>
<InvariantGlobalization>true</InvariantGlobalization>
Expand Down
2 changes: 1 addition & 1 deletion packaging/WINGET-SUBMISSION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<version>/`. 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.
Expand Down
4 changes: 2 additions & 2 deletions packaging/build-msix.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
40 changes: 38 additions & 2 deletions src/App.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ public sealed partial class App : Window
{
/// <summary>One navigation row and one contextual row before the main content.</summary>
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
Expand All @@ -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;
Expand Down Expand Up @@ -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 ()
{
Expand Down Expand Up @@ -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);
}
Expand All @@ -848,6 +868,7 @@ private void HideMainLoading ()
_mainLoadingMode = null;
_mainLoadingLabel.Visible = false;
_packageTable.Visible = true;
UpdateScoutVisibility ();
}

private void ShowMainLoadError (AppMode mode)
Expand All @@ -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) ";
}
Expand All @@ -879,6 +901,7 @@ private void RefreshTable ()
});

RefreshStatusBar ();
UpdateScoutVisibility ();

return;
}
Expand Down Expand Up @@ -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;
Comment thread
Copilot marked this conversation as resolved.
}

private void UpdateListTitle ()
{
string title = _state.Mode switch
Expand Down
49 changes: 49 additions & 0 deletions tests/AppBehaviorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<AppState> (app, "_state");
FrameView list = GetPrivateField<FrameView> (app, "_listFrame");
Label scout = GetPrivateField<Label> (app, "_scoutMascot");
LayoutView (app, new (100, 30));
state.Mode = mode;
state.Packages = [new () { Id = "Example.App", Name = "Example", Version = "1", Source = "winget" }];
state.ApplyFilter ();
InvokePrivate (app, "RefreshTable");
LayoutView (app, new (100, 30));

Assert.True (scout.Visible);
Assert.True (scout.Frame.Y >= state.Filtered.Count + 4);
Assert.True (scout.Frame.Right < list.Viewport.Width);
}

[Fact]
public void App_ScoutHidesWhenRowsFillPaneOrListIsLoadingOrFailed ()
{
App app = new (new MockBackend ()) { Frame = new (0, 0, 100, 30) };
AppState state = GetPrivateField<AppState> (app, "_state");
Label scout = GetPrivateField<Label> (app, "_scoutMascot");
LayoutView (app, new (100, 30));
state.Packages = Enumerable.Range (0, 30)
.Select (i => new Package { Id = $"Example.{i}", Name = $"Example {i}", Version = "1" }).ToList ();
state.ApplyFilter ();
InvokePrivate (app, "RefreshTable");
Assert.False (scout.Visible);

state.Packages = [new () { Id = "Example.App", Name = "Example", Version = "1" }];
state.ApplyFilter ();
InvokePrivate (app, "RefreshTable");
Assert.True (scout.Visible);

InvokePrivate (app, "ShowMainLoading", AppMode.Installed);
Assert.False (scout.Visible);

state.ViewError = "Could not load Installed. Press r to retry.";
InvokePrivate (app, "HideMainLoading");
InvokePrivate (app, "RefreshTable");
Assert.False (scout.Visible);
}

[Fact]
public void App_OpeningSearch_ShowsTheSearchFieldImmediately ()
{
Expand Down
4 changes: 3 additions & 1 deletion tests/ParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,9 @@ public void CompareVersionsLike_SortingDoesNotAllocatePerComparison ()
}

long allocated = GC.GetAllocatedBytesForCurrentThread () - before;
Assert.True (allocated < 1_024, $"Version comparisons allocated {allocated} bytes.");
// Tiered JIT and globalization can add a few fixed allocations on CI runners.
// A per-comparison object would still exceed this bound by a wide margin.
Assert.True (allocated < 8_192, $"Version comparisons allocated {allocated} bytes.");
}

[Fact]
Expand Down
Loading