functions/Start-PSCountdownTimer.ps1

Function Start-PSCountdownTimer {
    [CmdletBinding(DefaultParameterSetName = "seconds")]
    [OutputType("None")]
    Param(
        [Parameter(
            Position = 0,
            HelpMessage = "Enter seconds to countdown between 10 and 3600. The default is 60.",
            ParameterSetName="seconds"
        )]
        [ValidateRange(10,3600)]
        [Int]$Seconds = 60,

        [Parameter(
            Position = 0,
            Mandatory,
            ParameterSetName = "time",
            HelpMessage = "Enter a DateTime value as the countdown target."
        )]
        [ValidateNotNullOrEmpty()]
        #validate that the time is in the future
        [ValidateScript({ $_ -gt (Get-Date) })]
        [DateTime]$Time,

        [Parameter(HelpMessage = "Specify a short message prefix like 'Starting in: '")]
        [VaLidateNotNullOrEmpty()]
        [String]$Message,

        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateScript({ $_ -gt 8 })]
        [alias("size")]
        [Int]$FontSize = 48,

        [Parameter(HelpMessage = "Specify a font style.", ValueFromPipelineByPropertyName)]
        [ValidateSet("Normal", "Italic", "Oblique")]
        [alias("style")]
        [String]$FontStyle = "Normal",

        [Parameter(HelpMessage = "Specify a font weight.", ValueFromPipelineByPropertyName)]
        [ValidateSet("Normal", "Bold", "Light")]
        [alias("weight")]
        [String]$FontWeight = "Normal",

        [Parameter(HelpMessage = "Specify a font color like Green or an HTML code like '#FF1257EA'", ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [String]$Color = "White",

        [Parameter(HelpMessage = "Specify a font family.", ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [alias("family")]
        [String]$FontFamily = "Segoi UI",

        [Parameter(HelpMessage = "Do you want the clock to always be on top?", ValueFromPipelineByPropertyName)]
        [Switch]$OnTop,

        [Parameter(HelpMessage = "Specify the clock position as an array of left and top values.", ValueFromPipelineByPropertyName)]
        [ValidateCount(2, 2)]
        [Int32[]]$Position,

        [Parameter(HelpMessage = "Specify the number of seconds remaining to switch to alert coloring")]
        [ValidateScript({$_ -ge 1})]
        [Int]$Alert = 50,

        [Parameter(HelpMessage = "Specify alert coloring")]
        [ValidateNotNullOrEmpty()]
        [String]$AlertColor = "Yellow",

        [Parameter(HelpMessage = "Specify the number of seconds remaining to switch to warning coloring")]
        [ValidateScript({$_ -ge 1})]
        [Int]$Warning = 30,

        [Parameter(HelpMessage = "Specify warning coloring")]
        [ValidateNotNullOrEmpty()]
        [String]$WarningColor = "Red",

        [Parameter(HelpMessage = "Define a ScriptBlock to execute when the clock expires")]
        [ValidateNotNullOrEmpty()]
        [ScriptBlock]$Action
    )

    Begin {
        Write-Verbose "[$((Get-Date).TimeOfDay) BEGIN ] Starting $($MyInvocation.MyCommand)"
    } #begin
    Process {
        Write-Verbose "[$((Get-Date).TimeOfDay) PROCESS] Using parameter set $($psCmdlet.ParameterSetName)"
        if ($psCmdlet.ParameterSetName -eq 'time') {
            $Seconds = ($Time - (Get-Date)).TotalSeconds
        }
        Write-Verbose "[$((Get-Date).TimeOfDay) PROCESS] Using PSBoundParameters: `n $(New-Object PSObject -Property $PSBoundParameters | Out-String)"
        Write-Verbose "[$((Get-Date).TimeOfDay) PROCESS] Validating"
        if ($IsLinux -OR $isMacOS) {
            Write-Warning "This command requires a Windows platform."
            return
        }

        if ($global:PSCountdownClock.Running) {
            Write-Warning "You already have a clock running. You can only have one clock running at a time."
            $PSCountdownClock
            Return
        }

        if (Test-Path $env:temp\pscountdown-flag.txt) {
            $msg = @"
 
A running countdown clock has been detected from another PowerShell session:
 
$(Get-Content $env:temp\pscountdown-flag.txt)
 
If this is incorrect, delete $env:temp\pscountdown-flag.txt and try again.
 
"@

            Write-Warning $msg
            $r = Read-Host "Do you want to remove the flag file? Y/N"
            if ($r -eq 'Y') {
                Remove-Item $env:temp\pscountdown-flag.txt
            }
            else {
                #bail out
                Return
            }
        }

        #verify the DateTime format
        Try {
            [void](Get-Date -Format $DateFormat -ErrorAction Stop)
        }
        Catch {
            Write-Warning "The DateFormat value $DateFormat is not a valid format string. Try something like F,G, or U which are case-sensitive."
            Return
        }

        Write-Verbose "[$((Get-Date).TimeOfDay) PROCESS] Building a synchronized hashtable (`$PSCountDownClock)"
        $global:PSCountdownClock = [hashtable]::Synchronized(@{
                FontSize         = $FontSize
                FontStyle        = $FontStyle
                FontWeight       = $FontWeight
                Color            = $Color
                FontFamily       = $FontFamily
                OnTop            = $OnTop
                StartingPosition = $Position
                CurrentPosition  = $Null
                Seconds          = $seconds
                Message          = $Message
                Alert            = $alert
                Warning          = $Warning
                AlertColor       = $AlertColor
                WarningColor     = $WarningColor
                Action           = $Action
            })
        Write-Verbose "[$((Get-Date).TimeOfDay) PROCESS] $($global:PSCountdownClock | Out-String)"
        #Run the clock in a RunSpace
        $rs = [RunSpaceFactory]::CreateRunSpace()
        $rs.ApartmentState = "STA"
        $rs.ThreadOptions = "ReuseThread"
        $rs.Open()

        $global:PSCountdownClock.add("RunSpace", $rs)

        $rs.SessionStateProxy.SetVariable("PSCountdownClock", $global:PSCountdownClock)

        Write-Verbose "[$((Get-Date).TimeOfDay) PROCESS] Defining the RunSpace command"
        $psCmd = [PowerShell]::Create().AddScript({

                Add-Type -AssemblyName PresentationFramework -ErrorAction Stop
                Add-Type -AssemblyName PresentationCore -ErrorAction Stop
                Add-Type -AssemblyName WindowsBase -ErrorAction Stop

                # a private function to stop the clock and clean up
                Function _QuitClock {
                    $PSCountdownClock.Running = $False
                    $timer.stop()
                    $timer.IsEnabled = $False
                    $form.close()

                    #define a thread job to clean up the RunSpace
                    $cmd = {
                        Param([Int]$ID)
                        $r = Get-RunSpace -Id $id
                        $r.close()
                        $r.dispose()
                    }
                    Start-ThreadJob -ScriptBlock $cmd -ArgumentList $PSCountdownClock.RunSpace.id

                    #delete the flag file
                    if (Test-Path $env:temp\pscountdown-flag.txt) {
                        Remove-Item $env:temp\pscountdown-flag.txt
                    }

                    #Start the Action if specified
                    If ($PSCountdownClock.Action) {
                        Invoke-Command -ScriptBlock $PSCountdownClock.Action
                    }
                }

                $form = New-Object System.Windows.Window
                [Int]$script:i = $PSCountdownClock.seconds
                <#
                some of the form settings are irrelevant because it is transparent
                but leaving them in the event I need to turn off transparency
                to debug or troubleshoot
                #>


                $form.Title = "PSCountdownClock"
                $form.Height = 200
                $form.Width = 400
                $form.SizeToContent = "WidthAndHeight"
                $form.AllowsTransparency = $True
                $form.Topmost = $PSCountdownClock.OnTop

                $form.Background = "Transparent"
                $form.BorderThickness = "1,1,1,1"
                $form.VerticalAlignment = "top"

                if ($PSCountdownClock.StartingPosition) {
                    $form.left = $PSCountdownClock.StartingPosition[0]
                    $form.top = $PSCountdownClock.StartingPosition[1]
                }
                else {
                    $form.WindowStartupLocation = "CenterScreen"
                }
                $form.WindowStyle = "None"
                $form.ShowInTaskbar = $False

                #define events
                #call the private function to stop the clock and clean up
                $form.Add_MouseRightButtonUp({ _QuitClock })

                $form.Add_MouseLeftButtonDown({ $form.DragMove() })

                #press + to increase the size and - to decrease
                #the clock needs to refresh to see the result
                $form.Add_KeyDown({
                    switch ($_.key) {
                        { 'Add', 'OemPlus' -contains $_ } {
                            If ( $PSCountdownClock.fontSize -ge 8) {
                                $PSCountdownClock.fontSize++
                                $form.UpdateLayout()
                            }
                        }
                        { 'Subtract', 'OemMinus' -contains $_ } {
                            If ($PSCountdownClock.FontSize -ge 8) {
                                $PSCountdownClock.FontSize--
                                $form.UpdateLayout()
                            }
                        }
                    }
                })

                #fail safe to remove flag file
                $form.Add_Unloaded({
                    if (Test-Path $env:temp\pscountdown-flag.txt) {
                        Remove-Item $env:temp\pscountdown-flag.txt
                    }
                })

                $stack = New-Object System.Windows.Controls.StackPanel

                $label = New-Object System.Windows.Controls.label
                $ts = "{0} {1}" -f $PSCountdownClock.message,(New-TimeSpan -Seconds $script:i).ToString()
                $label.Content = $ts.Trim()
                #"Hello World"
                #Get-Date -Format $PSCountdownClock.DateFormat

                $label.HorizontalContentAlignment = "Center"
                $label.Foreground = $PSCountdownClock.Color
                $label.FontStyle = $PSCountdownClock.FontStyle
                $label.FontWeight = $PSCountdownClock.FontWeight
                $label.FontSize = $PSCountdownClock.FontSize
                $label.FontFamily = $PSCountdownClock.FontFamily

                $label.VerticalAlignment = "Top"

                $stack.AddChild($label)
                $form.AddChild($stack)

                $timer = New-Object System.Windows.Threading.DispatcherTimer
                $timer.Interval = [TimeSpan]"0:0:1.00"
                $timer.Add_Tick({
                        $script:i--
                        if ($PSCountdownClock.Running -AND ($script:i -gt 0)) {
                            #set the font to yellow at 20 seconds and red at 10 seconds
                            if ($script:i -le $PSCountdownClock.Warning) {
                                $label.Foreground = $PSCountdownClock.WarningColor
                            }
                            elseif ($script:i -le $PSCountdownClock.Alert) {
                                $label.foreground = $PSCountdownClock.AlertColor
                            }
                            else {
                                $label.Foreground = $PSCountdownClock.Color
                            }
                            $label.FontStyle = $PSCountdownClock.FontStyle
                            $label.FontWeight = $PSCountdownClock.FontWeight
                            $label.FontSize = $PSCountdownClock.FontSize
                            $label.FontFamily = $PSCountdownClock.FontFamily
                            $ts = "{0} {1}" -f $PSCountdownClock.message,(New-TimeSpan -Seconds $script:i).ToString()
                            $label.Content = $ts.Trim()
                            #"Hello World"
                            #Get-Date -Format $PSCountdownClock.DateFormat

                            $form.TopMost = $PSCountdownClock.OnTop
                            $form.UpdateLayout()

                            #$PSCountdownClock.Window = $Form
                            $PSCountdownClock.CurrentPosition = $form.left, $form.top
                        }
                        else {
                            _QuitClock
                        }
                    })
                $timer.Start()

                $PSCountdownClock.Running = $True
                $PSCountdownClock.Started = Get-Date

                #Show the clock form
                [void]$form.ShowDialog()
            })

        $psCmd.RunSpace = $rs
        Write-Verbose "[$((Get-Date).TimeOfDay) PROCESS] Launching the RunSpace"
        [void]$psCmd.BeginInvoke()

        Write-Verbose "[$((Get-Date).TimeOfDay) PROCESS] Creating the flag file $env:temp\pscountdown-flag.txt"
        "[{0}] PSClock started by {1} under PowerShell process id $pid" -f (Get-Date), $env:USERNAME |
        Out-File -FilePath $env:temp\pscountdown-flag.txt

    } #process
    End {
        Write-Verbose "[$((Get-Date).TimeOfDay) END ] Ending $($MyInvocation.MyCommand)"
    } #end

} #close function