Skip to content

Recipes

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

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.

Blocking notification

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"
}

Progress dialog driven from a script

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 SilentlyContinue

Live list of install steps

Add 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 SilentlyContinue

The 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.

Locked window the user cannot dismiss

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 UTF8

button1: 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.

Fullscreen deployment screen

$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-Null

The 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.

Show a pre-built list without live updates

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.

Calling from a system context

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 $principal

Using 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.

Reading the outcome

$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.

Clone this wiki locally