Skip to content

Development

Rod Christiansen edited this page Sep 3, 2026 · 1 revision

Development

Repository layout

taskbarutil/
├── src/
│   ├── Program.cs                 command tree, crash handler
│   ├── TaskbarUtil.csproj
│   ├── Commands/                  one file per verb
│   │   ├── AddCommand.cs
│   │   ├── ApplyCommand.cs
│   │   ├── FindCommand.cs
│   │   ├── ListCommand.cs
│   │   ├── MoveCommand.cs
│   │   ├── RemoveCommand.cs
│   │   ├── ReplaceCommand.cs
│   │   ├── ResetCommand.cs
│   │   ├── SettingsCommand.cs
│   │   └── ShowCommand.cs
│   ├── Core/
│   │   ├── AppResolver.cs         name -> pinnable identifier
│   │   ├── EnvironmentInfo.cs     every well-known path in one place
│   │   ├── ExplorerHelper.cs      cache clearing and shell restart
│   │   ├── FileLog.cs             rolled file log, shared verbatim across tools
│   │   ├── KnownApps.cs           built-in app table
│   │   ├── LayoutXmlGenerator.cs
│   │   ├── LayoutXmlParser.cs
│   │   ├── Log.cs                 lazy process-wide FileLog
│   │   ├── PinnedItemsReader.cs   legacy User Pinned folder
│   │   ├── PolicyManager.cs       policy write/clear, profile enumeration
│   │   ├── ShortcutRanker.cs      shortcut scoring
│   │   ├── TaskbarLayout.cs       ordered pin list
│   │   ├── TaskbarPin.cs
│   │   └── TaskbarWriter.cs       direct shortcut write + backup/restore
│   └── Models/
│       ├── PinType.cs
│       └── ResolvedApp.cs
├── tests/
│   ├── FileLogTests.cs
│   ├── LayoutXmlTests.cs
│   ├── ShortcutRankerTests.cs
│   └── TaskbarUtil.Tests.csproj
├── build/pkg/                     cimipkg staging templates
│   ├── build-info.yaml
│   ├── preinstall.ps1
│   └── postinstall.ps1
├── examples/                      sample setup scripts
├── .github/workflows/
│   ├── ci.yml
│   └── release.yml
├── build.ps1                      local build, sign, package
├── build-info.yaml                packaging metadata template
└── TaskbarUtil.sln

Prerequisites

Requirement Notes
.NET SDK 10.0 DOTNET_VERSION: '10.0.x' in both workflows
Windows Target framework is net10.0-windows10.0.22621.0; the code uses Microsoft.Win32.Registry, user32.dll P/Invoke and COM
Windows SDK Only for signing — supplies signtool.exe
gh CLI Only if you let build.ps1 download cimipkg for you

The only NuGet dependency in the product is System.CommandLine 2.0.0-beta4.22272.1. Tests use xunit 2.6.1, Microsoft.NET.Test.Sdk 17.8.0 and coverlet.

Building

dotnet build TaskbarUtil.sln

Full local build — publishes both runtime identifiers, packages MSI and nupkg, and zips the binaries into release\:

.\build.ps1
Switch Effect
-Build Publish binaries only
-Msi MSI packages only
-Nupkg NuGet packages only
-Test Run tests as part of the build; failure aborts
-Clean Remove dist, release and staging directories first
-Sign Force signing
-NoSign Skip signing entirely
-Configuration <name> Default Release
-Runtime <rids> Default win-x64, win-arm64
-CertificateName <cn> Certificate CN for signing
-Thumbprint <hash> Certificate thumbprint for signing

With no switches at all it turns on -Build, -Msi and -Nupkg.

Publish settings, matched by CI and the release workflow:

dotnet publish src/TaskbarUtil.csproj -c Release -r win-x64 --self-contained -p:PublishSingleFile=true -p:PublishTrimmed=true -p:TrimMode=partial

Output lands in src\bin\Release\net10.0-windows10.0.22621.0\<rid>\publish\taskbarutil.exe and is copied to dist\<arch>\.

IL2026 and IL2072 are suppressed in the csproj: the COM interop into WScript.Shell produces false-positive trim warnings. If you add real reflection, do not widen that suppression — the binary is trimmed and reflection over trimmed types will fail at runtime, not build time.

Local builds stamp the version as yyyy.MM.dd.HHmm; release builds use the tag.

Tests

dotnet test TaskbarUtil.sln

Three suites, all pure logic — nothing touches the registry, the shell or the Start Menu:

Suite Covers
LayoutXmlTests Generation of each pin element type, PinListPlacement, PinGeneration, and round-tripping through the parser
ShortcutRankerTests Scoring, secondary-marker and non-executable penalties, version and length tie-breaks
FileLogTests Line format, level names, rolling and generation shuffling

Anything touching PolicyManager, ExplorerHelper, AppResolver or TaskbarWriter is untested by design — it is inseparable from live machine state. Keep new logic in the pure layers where it can be tested, and keep the machine-state layers thin.

Release process

Releases are cut by pushing a v* tag. .github/workflows/release.yml does the rest on a windows-latest runner with a 90-minute timeout and contents: write.

git tag v1.2.3
git push origin v1.2.3

The workflow, in order:

  1. Derives the version by stripping the leading v from the tag name.
  2. dotnet restore TaskbarUtil.sln.
  3. Publishes win-x64 and win-arm64 self-contained, single-file, partially trimmed, with -p:Version=<version>, copying each to dist/<arch>/taskbarutil.exe.
  4. Downloads cimipkg-win-x64.zip from the latest windowsadmins/cimian-pkg release via gh release download and extracts it to tools/.
  5. For each architecture, builds a staging directory with payload/ and scripts/, substitutes {{VERSION}} and {{ARCHITECTURE}} into build/pkg/build-info.yaml, preinstall.ps1 and postinstall.ps1, then runs cimipkg to produce release/TaskbarUtil-<arch>-<version>.msi. A missing MSI aborts the run.
  6. Runs cimipkg --nupkg over the same staging directories for release/TaskbarUtil-<arch>-<version>.nupkg.
  7. Zips each binary to release/taskbarutil-<arch>.zip.
  8. Creates the GitHub release with gh release create, uploading everything in release/. The notes are assembled from a build-info block (SDK version, runner OS, a link to the workflow run), GitHub's auto-generated notes fetched from the releases/generate-notes API, and a signing block.

Release artifacts are unsigned — the hosted runner has no certificate — and the notes say so and give the signtool commands. Signing for enterprise distribution happens after the fact, or locally through build.ps1.

CI

.github/workflows/ci.yml runs on pushes and pull requests to main, on windows-latest, 30-minute timeout, contents: read, with in-progress runs cancelled per ref. It restores, builds Release, runs dotnet test with a TRX logger, and uploads TestResults/*.trx as an artifact even on failure. NuGet packages are cached on a key derived from the project files.

CI does not build packages and does not publish binaries.

Conventions

One command, one file. Each verb owns its options, its exit codes and its own log lines. Commands are static factory classes returning a Command, wired up in Program.cs.

Exit codes are set, not thrown. Commands assign Environment.ExitCode and return. 1 is a usage or state error, 3 is a not-found. Keep to that.

Log every mutation, at the point of mutation. Every pin, unpin, move, replace, apply, reset and settings change gets a log line naming the target and the outcome, including dry runs at DEBUG and failures at ERROR. Read-only commands log nothing so they never create the log directory.

Paths belong in EnvironmentInfo. Do not scatter Path.Combine of well-known folders through the commands.

Never let logging change behaviour. FileLog swallows every failure. It is copied verbatim between sibling utilities, so keep it dependency-free and do not add tool-specific knowledge to it.

Comment the surprising, not the obvious. The long comments in ShortcutRanker and ExplorerHelper document real field failures — an unrelated app pinned because "Code" matched inside "Encoder", a session left with no shell at all. Extend that style when you fix something non-obvious; the reasoning is the valuable part.

Nullable reference types and implicit usings are on in both projects. Keep them clean.

Clone this wiki locally