Repository navigation
Recipes
Working scripts using only flags and commands that exist. Each assumes dialog.exe is on
PATH; substitute the full path (C:\Program Files\csharpDialog\dialog.exe) if you are
calling it from a context that does not inherit the machine PATH, such as a service or a
scheduled task created before the install.
Two rules underpin all of them:
- Pass
--window(or--fullscreen/--kiosk) or the process falls back to a console prompt and blocks on stdin. - Sleep ~2 seconds after launching before writing the first command. The watcher starts about a second after the window is created.
A message the user has to acknowledge, with a hard time limit so an unattended machine does not sit on it forever. Exit code 0 means acknowledged, 1 means it timed out.
$dialogArgs = @(
"--window",
"--title", "Restart required",
"--message", "Updates have been installed. Please save your work and restart within the hour.",
"--button1", "I understand",
"--timeout", "3600"
)
$proc = Start-Process dialog -ArgumentList $dialogArgs -PassThru -Wait
if ($proc.ExitCode -eq 0) {
Write-Host "User acknowledged"
} else {
Write-Host "Notice timed out unacknowledged"
}The core pattern: launch, wait, append, quit. The progress bar's maximum is fixed at 100, so send percentages.
$commandFile = Join-Path $env:TEMP "dialog-$PID.txt"
Remove-Item $commandFile -Force -ErrorAction SilentlyContinue
$proc = Start-Process dialog -PassThru -ArgumentList @(
"--window", "--progress",
"--title", "Applying configuration",
"--message", "This usually takes two to three minutes.",
"--commandfile", $commandFile
)
Start-Sleep -Seconds 2
$tasks = @(
@{ Name = "Registry baseline"; Action = { Start-Sleep -Seconds 2 } }
@{ Name = "Firewall rules"; Action = { Start-Sleep -Seconds 3 } }
@{ Name = "Scheduled tasks"; Action = { Start-Sleep -Seconds 2 } }
)
for ($i = 0; $i -lt $tasks.Count; $i++) {
Add-Content $commandFile "progresstext: $($tasks[$i].Name)" -Encoding UTF8
& $tasks[$i].Action
Add-Content $commandFile "progress: $([math]::Round(($i + 1) / $tasks.Count * 100))" -Encoding UTF8
}
Add-Content $commandFile "progresstext: Finished" -Encoding UTF8
Start-Sleep -Seconds 2
Add-Content $commandFile "quit" -Encoding UTF8
Wait-Process -Id $proc.Id -Timeout 15 -ErrorAction SilentlyContinue
Remove-Item $commandFile -Force -ErrorAction SilentlyContinueAdd every row up front so the user can see the whole scope, then walk it. Update titles
must match the add titles character for character.
$commandFile = Join-Path $env:TEMP "install-$PID.txt"
Remove-Item $commandFile -Force -ErrorAction SilentlyContinue
$packages = "Google Chrome", "Zoom", "Adobe Acrobat Reader", "7-Zip"
$proc = Start-Process dialog -PassThru -ArgumentList @(
"--window", "--progress",
"--title", "Installing applications",
"--message", "Leave this window open. It will close when everything is installed.",
"--commandfile", $commandFile
)
Start-Sleep -Seconds 2
foreach ($pkg in $packages) {
Add-Content $commandFile "listitem: add, title: $pkg, status: wait, statustext: Queued" -Encoding UTF8
}
$done = 0
foreach ($pkg in $packages) {
Add-Content $commandFile "listitem: update, title: $pkg, status: pending, statustext: Installing" -Encoding UTF8
Add-Content $commandFile "progresstext: Installing $pkg" -Encoding UTF8
try {
Install-MyPackage -Name $pkg -ErrorAction Stop
Add-Content $commandFile "listitem: update, title: $pkg, status: success, statustext: Installed" -Encoding UTF8
}
catch {
Add-Content $commandFile "listitem: update, title: $pkg, status: fail, statustext: Failed" -Encoding UTF8
}
$done++
Add-Content $commandFile "progress: $([math]::Round($done / $packages.Count * 100))" -Encoding UTF8
}
Add-Content $commandFile "message: Installation finished." -Encoding UTF8
Add-Content $commandFile "progresstext: Done" -Encoding UTF8
Start-Sleep -Seconds 3
Add-Content $commandFile "quit" -Encoding UTF8
Wait-Process -Id $proc.Id -Timeout 20 -ErrorAction SilentlyContinue
Remove-Item $commandFile -Force -ErrorAction SilentlyContinueThe statustext is worth using for anything the status dot cannot carry — a size, a
version, an error summary — because a failed row's text is the only place the user will see
why.
The swiftDialog "locked provisioning dialog" pattern. The three flags together disable the
button, refuse the title-bar close and Alt+F4, and swallow Esc/Enter, while --quitkey
leaves the operator one deliberate way out.
$commandFile = Join-Path $env:ProgramData "ManagedNotifications\work\provision.txt"
New-Item -ItemType Directory -Path (Split-Path $commandFile) -Force | Out-Null
Remove-Item $commandFile -Force -ErrorAction SilentlyContinue
$proc = Start-Process dialog -PassThru -ArgumentList @(
"--window", "--progress",
"--title", "Setting up this computer",
"--message", "Please do not restart or sign out.",
"--button1", "Please wait",
"--button1disabled",
"--hidedefaultkeyboardaction",
"--quitkey", "0",
"--commandfile", $commandFile
)When the work finishes, hand the window back to the user rather than closing it out from under them:
Add-Content $commandFile "message: Setup is complete." -Encoding UTF8
Add-Content $commandFile "button1text: Get started" -Encoding UTF8
Add-Content $commandFile "button1: enable" -Encoding UTF8button1: enable releases the close lock as well as re-enabling the button, so the user can
now close the window normally and the process exits 0.
Use --kiosk instead of --window when the dialog must also cover the desktop and cannot
be closed at all — but note that in kiosk mode the only exits are a quit command and a
timeout, so always give the script a failure path that sends quit.
$commandFile = Join-Path $env:ProgramData "ManagedNotifications\work\deploy.txt"
New-Item -ItemType Directory -Path (Split-Path $commandFile) -Force | Out-Null
Start-Process dialog -ArgumentList @(
"--fullscreen", "--progress",
"--title", "Preparing your computer",
"--message", "This can take 30 minutes or more. Your computer will restart when it is ready.",
"--commandfile", $commandFile
) | Out-NullThe dialog is centred on a dimmed, blurred backdrop covering every monitor. On Windows 10 and pre-22H2 Windows 11 the acrylic request fails silently and you get the plain dim.
For a static summary — a compliance report, a list of what a maintenance window did — the
list can be built entirely on the command line. No command file is needed, but --window
still is.
dialog --window `
--title "Maintenance summary" `
--message "Last night's maintenance window completed." `
--listitem "Windows updates,success" `
--listitem "Antivirus definitions,success" `
--listitem "Disk cleanup,wait" `
--listitem "Driver package,fail" `
--button1 "Close"Remember the comma is the field separator, so item titles cannot contain commas.
A dialog started by SYSTEM from a service or a scheduled task set to run as SYSTEM draws in session 0, where no user can see it — the process runs, the log fills in, and the user sees nothing. csharpDialog has no session-shifting code of its own.
Register a task that runs in the interactive user's session instead:
$action = New-ScheduledTaskAction -Execute "C:\Program Files\csharpDialog\dialog.exe" `
-Argument '--window --title "Maintenance" --message "Starting now." --timeout 120'
$principal = New-ScheduledTaskPrincipal -GroupId "S-1-5-32-545" -RunLevel Limited
Register-ScheduledTask -TaskName "csharpDialog notice" -Action $action -Principal $principalUsing the Users group SID rather than a name keeps this working on non-English installs. See Troubleshooting for how to confirm which session a run landed in.
$proc = Start-Process dialog -Wait -PassThru -ArgumentList @(
"--window", "--title", "Proceed?", "--message", "Install now?", "--button1", "Install", "--timeout", "60"
)
switch ($proc.ExitCode) {
0 { "User pressed the button" }
1 { "Timed out, quit key, or an error" }
}Exit code 1 covers several distinct outcomes. If you need to tell them apart, capture
stdout and look for the Result: line — timeout, quit, or an error message on stderr.
Do not assume the stream is clean; the current build emits diagnostic lines alongside it.
csharpDialog — MIT licensed — windowsadmins/csharpdialog
Reference
Guides
Internals
Help