WakieWakie.psm1

<#
. [COPYRIGHT]
. © 2011-2018 Microsoft Corporation. All rights reserved.
.
. [DISCLAIMER]
. This sample script is not supported under any Microsoft standard support
. program or service. The sample scripts are provided AS IS without warranty of
. any kind. Microsoft disclaims all implied warranties including, without
. limitation, any implied warranties of merchantability or of fitness for a
. particular purpose. The entire risk arising out of the use or performance of
. the sample scripts and documentation remains with you. In no event shall
. Microsoft, its authors, or anyone else involved in the creation, production,
. or delivery of the scripts be liable for any damages whatsoever (including,
. without limitation, damages for loss of business profits, business
. interruption, loss of business information, or other pecuniary loss) arising
. out of the use of or inability to use the sample scripts or documentation,
. even if Microsoft has been advised of the possibility of such damages.
.
. [AUTHOR]
. Jason Parker, Senior Consultant
.
. [CONTRIBUTORS]
.
.
. [MODULE]
. WakieWakie.psm1
.
. [VERSION]
. 1.0
.
. [VERSION HISTORY / UPDATES]
. 1.0 - Jason Parker
. Original Release
.
#>


Function Global:Start-WakieWakie {
    <#
    .SYNOPSIS
    Launches the WakieWakie application
 
    .DESCRIPTION
    WPF XAML based application used to send Numlock or Capslock key presses on a specific interval to help keep system from going to sleep or standby.
 
    .EXAMPLE
    Launches the application
 
    Start-WakieWakie
 
    .NOTES
    #>

    [CmdletBinding()]
    Param()
    # Adds WPF Assemblies
    Add-Type -Assembly PresentationFramework            
    Add-Type -Assembly PresentationCore

    # Clean up previous runspace
    $CheckRunspace = Get-Runspace -Name WakieWakie
    If (($CheckRunspace | Measure-Object).Count -ge 1) {
        $CheckRunspace | ForEach-Object {
            $_.Close()
            Start-Sleep -Milliseconds 500
            $_.Dispose()
        }
    }

    # Create a Hashtable that will span across multiple runspaces
    $Script:syncHash = [hashtable]::Synchronized(@{})
    $syncHash.Host = $Host

    # Prevents hard coding the XAML Path
    $ModulePath = (Get-Module -ListAvailable WakieWakie).Path
    $ModulePathLIO = $ModulePath.LastIndexOf("\")
    $syncHash.XAMLPath = ("{0}\XAML" -f $ModulePath.Substring(0,$ModulePathLIO))

    #$syncHash.XAMLPath = Join-Path $PSScriptRoot '\WakieWakie'
    #$PathLIO = $PSCmdlet.MyInvocation.MyCommand.Source.LastIndexOf("\")
    #$ScriptPath = $PSCmdlet.MyInvocation.MyCommand.Source.Substring(0,$PathLIO)
    #$syncHash.ScriptModule = "$ScriptPath\wakiecommon.ps1"

    $synchash.StopwatchTimeElapsed = [timespan]::Zero
    $syncHash.CountdownIterations = 0

    # Create, Configure, and Open Runspace for the Main UI
    $InitialSessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
    $Runspace = [runspacefactory]::CreateRunspace($InitialSessionState)
    $Runspace.ThreadOptions = "ReuseThread"
    $Runspace.ApartmentState = "STA"
    $Runspace.Name = "WakieWakie"

    $PowerShell = [powershell]::Create()
    $PowerShell.Runspace = $Runspace

    $Runspace.Open()
    $Runspace.SessionStateProxy.SetVariable("syncHash",$syncHash) # Adds the sync'd Hashtable to runspace


    [void]$PowerShell.AddScript({
        #. $syncHash.ScriptModule
        #$wpf = Get-ChildItem -Path $syncHash.XAMLPath -Filter *.xaml -File | Where-Object {$_.Name -ne "App.xaml"} | Get-XamlObject

        # Loads the XAML content into $wpf variable then adds all the properties from the XAML to the $syncHash
        $wpf = Get-ChildItem -Path $syncHash.XAMLPath -Filter *.xaml -File | Get-XamlObject
        $wpf.GetEnumerator() | ForEach-Object {$script:syncHash.Add($_.Name,$_.Value)}

        # Scriptblock executed during every iteration of the UI Timer which is what updates the Main portions of the UI for responsiveness
        $updateblock = {
            $CLStatus = [System.Console]::CapsLock
            $NLStatus = [System.Console]::NumberLock
            If ($CLStatus) {$syncHash.CLStatusIndicator.Background = "#FF0CD3FF"}
            Else {$syncHash.CLStatusIndicator.Background = "#FFD1D1D1"}

            If ($NLStatus) {$syncHash.NLStatusIndicator.Background = "#FF0CD3FF"}
            Else {$syncHash.NLStatusIndicator.Background = "#FFD1D1D1"}

            $synchash.ElapsedTime_lbl.Content = ("{0}:{1}:{2}.{3}" -f $syncHash.StopwatchTimeElapsed.Hours,$syncHash.StopwatchTimeElapsed.Minutes,$syncHash.StopwatchTimeElapsed.Seconds,$syncHash.StopwatchTimeElapsed.Milliseconds.ToString('D3'))
            $synchash.LoopCounter_lbl.Content = $syncHash.CountdownIterations
        }

        # Creates the UI timer
        $UItimer = New-Object System.Windows.Threading.DispatcherTimer     
        $UItimer.Interval = [TimeSpan]::FromMilliseconds(1)
        $UItimer.Add_Tick($updateblock)
        $UItimer.Start()
        
        # Sets the MainWindow Frame object to load the Config.xaml page
        [Void]$syncHash.MainWindowFrame.NavigationService.Navigate($syncHash.Config)
        
        $syncHash.DurationTextBox.Add_TextChanged({
            # Validation check that the duration is a number between 1 and 30
            $DurationCheck = Compare-Object -ReferenceObject $(1..30) -DifferenceObject ([int]$this.Text) -IncludeEqual -ExcludeDifferent
            If ($DurationCheck) {
                $synchash.DurationBorder.BorderBrush = "gray"

                # Configurable option to determine the maximum run time
                $SyncHash.StopwatchMax = [timespan]::FromDays(5) 
                
                # Based on the duration, determines how many loops will be executed across the maximum run time
                $syncHash.TotalRuns = [Math]::Round($SyncHash.StopwatchMax.Ticks/([timespan]::FromMinutes($synchash.DurationTextBox.Text)).Ticks)
                $syncHash.MaximumLoops_lbl.Content = ("{0:N0}" -f $syncHash.TotalRuns)
                $synchash.btnStart.IsEnabled = $true
            }
            Else {
                $synchash.DurationBorder.BorderBrush = "red"
                $synchash.btnStart.IsEnabled = $false
            }
        })

        $syncHash.NLRadioButton.add_Checked({
            $syncHash.WakieKey = "NUMLOCK"
            $syncHash.KeySelectionBorder.BorderThickness = 0 # Clears the border when the radio button is checked
        })

        $syncHash.CLRadioButton.add_Checked({
            $syncHash.WakieKey = "CAPSLOCK"
            $syncHash.KeySelectionBorder.BorderThickness = 0 # Clears the border when the radio button is checked
        })

        $syncHash.btnStart.add_Click({
            # Validates that a key selection has been made
            If ($synchash.NLRadioButton.IsChecked -or $synchash.CLRadioButton.IsChecked) {
                # Cleans up any previous Countdown runspaces
                $CountdownRunspace = Get-Runspace -Name Countdown
                If (($CountdownRunspace | Measure-Object).Count -ge 1) {
                    $CountdownRunspace | ForEach-Object {
                        $_.Close()
                        Start-Sleep -Milliseconds 500
                        $_.Dispose()
                    }
                }
                New-CountdownTimer -syncHash $synchash -Duration $synchash.DurationTextBox.Text
            }
            Else {$syncHash.KeySelectionBorder.BorderThickness = 2} # Sets the key selection border to red if no option was checked
        })

        $syncHash.btnStop.add_Click({
            # Cleans up any previous Countdown runspaces
            $CountdownRunspace = Get-Runspace -Name Countdown
            If (($CountdownRunspace | Measure-Object).Count -ge 1) {
                $CountdownRunspace | ForEach-Object {
                    $_.Close()
                    Start-Sleep -Milliseconds 500
                    $_.Dispose()
                }
            }
            $synchash.DurationTextBox.IsEnabled = $true
            $synchash.btnStart.IsEnabled = $true
        })

        # Launches the window
        [Void]$syncHash.Window.ShowDialog()

    })

    [Void]$PowerShell.BeginInvoke()
    #$AsyncObject = $PowerShell.BeginInvoke()
}

Function Send-Keys {
    [CmdletBinding()]
    Param ([ValidateSet("CAPSLOCK","NUMLOCK","SCROLLLOCK")]$Key)
    
    $Shell = New-Object -ComObject wscript.shell
    $Shell.SendKeys("{$Key}")
    Start-Sleep -Milliseconds 500
    $Shell.SendKeys("{$Key}")
}

Function Get-XamlObject {
    [CmdletBinding()]
    param(
        [Parameter(Position = 0,
            Mandatory = $true,
            ValuefromPipelineByPropertyName = $true,
            ValuefromPipeline = $true)]
        [Alias("FullName")]
        [System.String[]]$Path
    )

    BEGIN
    {
        Set-StrictMode -Version Latest
        $expandedParams = $null
        $PSBoundParameters.GetEnumerator() | ForEach-Object { $expandedParams += ' -' + $_.key + ' '; $expandedParams += $_.value }
        Write-Verbose "Starting: $($MyInvocation.MyCommand.Name)$expandedParams"
        $output = @{ }
        Add-Type -AssemblyName presentationframework, presentationcore
    } #BEGIN

    PROCESS {
        try
        {
            foreach ($xamlFile in $Path)
            {
                #Wait-Debugger
                #Change content of Xaml file to be a set of powershell GUI objects
                $inputXML = Get-Content -Path $xamlFile -ErrorAction Stop
                [xml]$xaml = $inputXML -replace 'mc:Ignorable="d"', '' -replace "x:N", 'N' -replace 'x:Class=".*?"', '' -replace 'd:DesignHeight="\d*?"', '' -replace 'd:DesignWidth="\d*?"', ''
                $tempform = [Windows.Markup.XamlReader]::Load((New-Object System.Xml.XmlNodeReader $xaml -ErrorAction Stop))

                #Grab named objects from tree and put in a flat structure using Xpath
                $namedNodes = $xaml.SelectNodes("//*[@*[contains(translate(name(.),'n','N'),'Name')]]")
                $namedNodes | ForEach-Object {
                    $output.Add($_.Name, $tempform.FindName($_.Name))
                } #foreach-object
            } #foreach xamlpath
        } #try
        catch
        {
            throw $error[0]
        } #catch
    } #PROCESS

    END
    {
        Write-Output $output
        Write-Verbose "Finished: $($MyInvocation.Mycommand)"
    } #END
}

Function New-CountdownTimer {
    Param($syncHash,$Duration)
    $Runspace = [runspacefactory]::CreateRunspace()
    $Runspace.ApartmentState = "STA"
    $Runspace.ThreadOptions = "ReuseThread"
    $Runspace.Name = "Countdown"
    $Runspace.Open()
    $Runspace.SessionStateProxy.SetVariable("syncHash",$syncHash)
    $Runspace.SessionStateProxy.SetVariable("Duration",$Duration)

    $code = {
        #$syncHash.Host.UI.WriteVerboseLine("code begin")
        $syncHash.Window.Dispatcher.Invoke([action]{
            $syncHash.DurationTextBox.IsEnabled = $false
            $syncHash.btnStart.IsEnabled = $false
        })

        $Shell = New-Object -ComObject wscript.shell
        $DurationTimespan = [timespan]::FromMinutes($Duration)

        $StopWatch = New-Object System.Diagnostics.Stopwatch
        $StopWatch.Start()

        #$synchash.Host.UI.WriteVerboseLine("start stopwatch")
        $synchash.Flag = $true
        $i = 1
        Do {
            $synchash.StopwatchTimeElapsed = $StopWatch.Elapsed
            $syncHash.CountdownIterations = $i

            If ($StopWatch.Elapsed.TotalMinutes -ge $DurationTimespan.TotalMinutes) {
                
                $Shell.SendKeys("{$($synchash.WakieKey)}")
                Start-Sleep -Milliseconds 500
                $Shell.SendKeys("{$($synchash.WakieKey)}")
                
                #Send-Keys -Key $syncHash.WakieKey
                $syncHash.Flag = $false
                $StopWatch.Restart()
                $i++
            }

            Start-Sleep -Milliseconds 50
        } Until ($syncHash.Flag -eq $false -and $syncHash.CountdownIterations -eq $syncHash.TotalRuns)
        $StopWatch.Stop()
        #$synchash.Host.UI.WriteVerboseLine("stop stopwatch")

        $syncHash.Window.Dispatcher.Invoke([action]{
            $syncHash.DurationTextBox.IsEnabled = $true
            $syncHash.btnStart.IsEnabled = $true
        })
        #$synchash.Host.UI.WriteVerboseLine("code end")
    }

    $PSInstance = [powershell]::Create().AddScript($code)
    $PSInstance.runspace = $Runspace
    #$job = $PSInstance.BeginInvoke()
    [Void]$PSInstance.BeginInvoke()
}