This document explains how csharpDialog handles application icons, mirroring the workflow used by swiftDialog and Munki on macOS.
csharpDialog uses a repository-based icon system where icons are treated as deployment assets, not extracted from installed applications. This approach ensures icons are available before applications are installed, enabling rich progress dialogs during first-run device setup.
On macOS, Munki's Managed Software Center displays beautiful app icons during installation. Here's how:
- Source of Truth: An
icons/folder in the Munki repository (e.g.,https://munki.company.com/repo/icons/) - Icon Creation:
munkiimportextracts the app's ICNS during packaging- Converts to PNG and stores in
icons/folder - Manual placement for packages without extractable icons
- Manifest Reference: Each pkginfo specifies
icon_name(e.g.,Firefox.png) - Client Behavior:
- MSC fetches icons from
https://<repo>/icons/<icon_name>during catalog updates - Caches locally (no need for the app to be installed)
- Falls back to generic icons if unavailable
- MSC fetches icons from
Cimian Deployment Repository (Azure Blob / HTTPS)
└── deployment/
├── pkgsinfo/
│ └── apps/
│ ├── chrome.yaml (references icon_name: chrome.png)
│ ├── firefox.yaml
│ ├── zoom.yaml
│ └── vscode.yaml
├── icons/
│ ├── chrome.png (512×512, transparent PNG)
│ ├── firefox.png
│ ├── zoom.png
│ ├── vscode.png
│ └── _generic.png (fallback icon)
├── pkgs/
│ ├── chrome-120.0-x64.cimipkg
│ ├── firefox-126.0-x64.cimipkg
│ ├── zoom-5.17.0-x64.cimipkg
│ └── vscode-1.94.0-x64.cimipkg
└── catalogs/
└── production.json (aggregated catalog)
Repository Layout:
https://cimian.company.com/deployment/icons/<icon_name>.png
Cimian Icon Cache (Primary):
C:\ProgramData\ManagedInstalls\icons\<icon_name>.png
Icons are downloaded by Cimian during manifest sync and cached here. This is the primary location csharpDialog reads from.
csharpDialog Fallback Cache (Optional):
C:\ProgramData\csharpDialog\IconCache\<icon_name>.png
Used only when downloading icons directly via URL (for testing or when Cimian cache is unavailable).
Format Requirements:
- Format: PNG with alpha transparency
- Size: 512×512 pixels (or 256×256 minimum)
- Naming: Lowercase, alphanumeric + hyphens (e.g.,
google-chrome.png) - Compression: Optimized PNG (use pngcrush or similar)
Avoid:
- ❌ ICO format (Windows-specific, harder to serve over HTTP)
- ❌ SVG (requires rendering, inconsistent support)
- ❌ JPEG (no transparency)
- ❌ Very large files (> 200KB)
Package info stored at deployment/pkgsinfo/apps/chrome.yaml:
name: GoogleChrome
display_name: Google Chrome
version: 120.0.6099.130
icon_name: chrome.png # Basename only - resolved to deployment/icons/chrome.png
category: Browser
description: Fast, secure web browser from Google
installer:
type: msi
path: pkgs/chrome-120.0-x64.cimipkg
arguments: /quiet /norestart
minimum_os_version: '10.0'Note: icon_name uses basename only. Cimian resolves this to deployment/icons/chrome.png automatically.
csharpDialog resolves icons in this order:
- Cimian icon cache:
C:\ProgramData\ManagedInstalls\icons\<icon_name>(primary source) - Display name conversion: Converts display name to filename (e.g., "Google Chrome" →
chrome.png) - Generic fallback:
C:\ProgramData\ManagedInstalls\icons\_generic.png - Built-in status icons: Unicode emoji-based status indicators (⏳✅❌)
Note: Unlike the general documentation examples, when using Cimian integration, icons are ALWAYS read from Cimian's cache directory, not downloaded directly by csharpDialog.
Create icons during package creation using PowerShell:
# Extract icon from EXE
function Extract-ExeIcon {
param(
[string]$ExePath,
[string]$OutputPng,
[int]$Size = 512
)
Add-Type -AssemblyName System.Drawing
$icon = [System.Drawing.Icon]::ExtractAssociatedIcon($ExePath)
$bitmap = $icon.ToBitmap()
# Resize to desired size
$resized = New-Object System.Drawing.Bitmap($Size, $Size)
$graphics = [System.Drawing.Graphics]::FromImage($resized)
$graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
$graphics.DrawImage($bitmap, 0, 0, $Size, $Size)
$resized.Save($OutputPng, [System.Drawing.Imaging.ImageFormat]::Png)
$graphics.Dispose()
$resized.Dispose()
$bitmap.Dispose()
$icon.Dispose()
}
# Extract icon from MSI
function Extract-MsiIcon {
param(
[string]$MsiPath,
[string]$OutputPng
)
$installer = New-Object -ComObject WindowsInstaller.Installer
$database = $installer.GetType().InvokeMember("OpenDatabase", "InvokeMethod", $null, $installer, @($MsiPath, 0))
# Query for ARPPRODUCTICON property
$query = "SELECT Value FROM Property WHERE Property = 'ARPPRODUCTICON'"
$view = $database.GetType().InvokeMember("OpenView", "InvokeMethod", $null, $database, ($query))
$view.GetType().InvokeMember("Execute", "InvokeMethod", $null, $view, $null)
$record = $view.GetType().InvokeMember("Fetch", "InvokeMethod", $null, $view, $null)
if ($record) {
$iconName = $record.GetType().InvokeMember("StringData", "GetProperty", $null, $record, 1)
# Extract the icon file from MSI
# (Additional implementation needed to extract binary from Icon table)
}
}
# Extract icon from MSIX/AppX
function Extract-MsixIcon {
param(
[string]$MsixPath,
[string]$OutputPng
)
# Expand MSIX to temp location
$tempDir = New-Item -ItemType Directory -Path "$env:TEMP\msix_extract_$(Get-Random)"
Expand-Archive -Path $MsixPath -DestinationPath $tempDir
# Parse AppxManifest.xml for logo paths
[xml]$manifest = Get-Content "$tempDir\AppxManifest.xml"
$logo = $manifest.Package.Properties.Logo
# Find largest logo asset
$logoPath = "$tempDir\$($logo -replace '\\', '/')"
if (Test-Path $logoPath) {
Copy-Item $logoPath $OutputPng
}
Remove-Item $tempDir -Recurse -Force
}
# Usage example
Extract-ExeIcon -ExePath "C:\Temp\chrome_installer.exe" -OutputPng "icons\chrome.png"When extraction isn't possible:
- Download vendor assets: Check the software vendor's press/media kit
- Screenshot and crop: Run the app, screenshot the icon from the Start Menu
- Use generic: Create or find a representative icon
- Tools:
- GIMP - Free image editor
- Paint.NET - Windows image editor
- IconArchive - Icon resources
After creating icons, optimize them:
# Using pngquant (install via: choco install pngquant)
pngquant --quality=65-80 --ext .png --force icons/*.png
# Or using ImageMagick
magick convert icon.png -resize 512x512 -quality 95 icon_optimized.png# Icon from URL (with caching)
csharpdialog --listitem "title=Chrome,icon_url=https://cdn.company.com/icons/chrome.png,status=pending"
# Icon from local cache
csharpdialog --listitem "title=Chrome,icon=chrome.png,status=pending"
# Multiple items from JSON
csharpdialog --json-file packages.json{
"title": "Setting up your device",
"listitems": [
{
"title": "Google Chrome",
"icon_url": "https://cimian.company.com/deployment/icons/chrome.png",
"status": "pending",
"statustext": "Waiting to install"
},
{
"title": "Zoom",
"icon_url": "https://cimian.company.com/deployment/icons/zoom.png",
"status": "pending"
},
{
"title": "Microsoft Office",
"icon_name": "office.png",
"status": "pending"
}
],
"icon_base_url": "https://cimian.company.com/deployment/icons/"
}using csharpDialog.Core.Models;
using csharpDialog.Core.Services;
var config = new DialogConfiguration
{
Title = "Setting up your device",
ListItems = new List<ListItemConfiguration>
{
new ListItemConfiguration("Google Chrome")
{
Icon = "https://cimian.company.com/deployment/icons/chrome.png",
Status = ListItemStatus.Pending
},
new ListItemConfiguration("Zoom")
{
Icon = "zoom.png", // Resolves to base URL + icon name
Status = ListItemStatus.Pending
}
},
IconBaseUrl = "https://cimian.company.com/deployment/icons/"
};
var service = DialogServiceFactory.CreateDialogService(config);
await service.ShowDialogAsync(config);- First fetch: Downloads from URL, stores in local cache
- Subsequent use: Loads from cache (instant)
- Cache duration: 30 days (configurable)
- Cache invalidation:
- Manual: Delete
C:\ProgramData\csharpDialog\IconCache\ - Automatic: Old files cleaned up after 90 days
- Manual: Delete
# Clear icon cache
Remove-Item "C:\ProgramData\csharpDialog\IconCache\*" -Recurse -Force
# Pre-populate cache (for offline scenarios)
$icons = @("chrome.png", "zoom.png", "office.png")
$baseUrl = "https://cimian.company.com/deployment/icons/"
foreach ($icon in $icons) {
Invoke-WebRequest -Uri "$baseUrl$icon" -OutFile "C:\ProgramData\csharpDialog\IconCache\$icon"
}-
Package creation: Extract icon during package build
Extract-ExeIcon -ExePath "chrome.exe" -OutputPng "deployment/icons/chrome.png"
-
Package info creation: Reference icon by basename in YAML
# deployment/pkgsinfo/apps/chrome.yaml icon_name: chrome.png
-
Upload to repository: Both package and icon to deployment repo
deployment/ icons/chrome.png pkgsinfo/apps/chrome.yaml pkgs/chrome-120.0-x64.cimipkg -
Catalog generation: Aggregate package info into catalog
New-CimianCatalog -OutputPath "deployment/catalogs/production.json"
-
Client sync: Cimian downloads manifest and icon cache
C:\ProgramData\ManagedInstalls\ manifests\staff.json icons\chrome.png ← Downloaded by Cimian -
Dialog display: csharpDialog reads icons from Cimian's cache
csharpdialog --cimian --firstrun
-
Progress updates: Status updates as Cimian installs packages
# Read Cimian manifest
$manifest = Get-Content "C:\ProgramData\ManagedInstalls\manifests\staff.json" | ConvertFrom-Json
# Build dialog configuration
$listItems = @()
foreach ($package in $manifest.managed_installs) {
# Icon resolution: Cimian has already cached icons locally
$iconPath = "C:\ProgramData\ManagedInstalls\icons\$($package.icon_name)"
# Fallback to generic if specific icon missing
if (-not (Test-Path $iconPath)) {
$iconPath = "C:\ProgramData\ManagedInstalls\icons\_generic.png"
}
$listItems += @{
title = $package.display_name
icon = $iconPath # Local path from Cimian's cache
status = "pending"
}
}
# Launch dialog
$json = @{
title = "Setting up your device"
message = "Please wait while we install your applications"
listitems = $listItems
commandfile = "C:\ProgramData\csharpDialog\cimian-progress.log"
} | ConvertTo-Json -Depth 10
$json | Out-File "C:\ProgramData\csharpDialog\config.json"
# Start dialog with Cimian monitoring
Start-Process csharpdialog -ArgumentList "--json-file C:\ProgramData\csharpDialog\config.json --cimian"
# Monitor Cimian log and update dialog
# (Implementation in separate monitoring script - see Cimian-Integration.md)- Standardize icon sizes: Use 512×512 consistently
- Optimize files: Keep icons under 100KB each
- Use CDN: Host icons on fast, reliable infrastructure
- Test offline: Ensure graceful fallbacks when icons can't load
- Version control: Track icon changes alongside packages
- Naming convention: Use consistent, descriptive filenames
- Extract during packaging: Automate icon extraction in build scripts
- Test rendering: Verify icons look good on light and dark backgrounds
- Include metadata: Document icon source and license in manifest
- Fallback category: Specify category for generic icon fallback
- High quality: Use official vendor assets when possible
Icons are managed automatically - no user intervention required!
- Check URL: Verify icon URL is accessible
- Check cache: Look in
C:\ProgramData\csharpDialog\IconCache\ - Check format: Ensure PNG format with transparency
- Check size: Very large files may timeout
- Check permissions: Ensure cache directory is writable
- Use local caching: Pre-populate cache for offline scenarios
- Optimize PNGs: Compress images to reduce download time
- Use CDN: Serve icons from fast, geographically distributed servers
- Limit concurrent: Download icons sequentially, not all at once
This is expected behavior! Generic icons display when:
- Icon URL is unavailable
- Network is offline
- Icon file is corrupt
- No icon specified in manifest
For questions or improvements to this workflow, please open an issue on the csharpDialog repository.