Add-Icicle.ps1

function Add-Icicle
{
    <#
    .Synopsis
        Adds an Icicle to the ISE
    .Description
        Adds an icicle to the ISE. Icicles are mini-apps for the PowerShell ISE.
    .Link
        Import-Icicle
    .Example
        Add-Icicle -Horizontal -Name Clock -Screen {
            New-Border -Child {
                New-Label "$(Get-Date | Out-String)" -FontSize 24 -FontFamily 'Lucida Console'
            }
        } -DataUpdate {
            Get-date
        } -UiUpdate {
            $this.Content.Child.Content = $args | Out-String
        } -UpdateEvery "0:0:1"
    .Example
        Add-Icicle -Command (Get-Command Get-Process)
    #>

    [CmdletBinding(DefaultParameterSetName='Site')]
    param(
    # The name of the icicle
    [Parameter(ParameterSetName='Command')]
    [Parameter(Mandatory=$true,ParameterSetName='Site',ValueFromPipelineByPropertyName=$true)]
    [Parameter(Mandatory=$true,ParameterSetName='Screen',ValueFromPipelineByPropertyName=$true)]
    [Parameter(Mandatory=$true,ParameterSetName='UpdatedScreen',ValueFromPipelineByPropertyName=$true)]
    [Parameter(Mandatory=$true,ParameterSetName='UpdatedSite',ValueFromPipelineByPropertyName=$true)]
    [string]
    $Name,    

    # The url to display in the icicle
    [Parameter(Mandatory=$true, ParameterSetName='Site',ValueFromPipelineByPropertyName=$true)]
    [Uri]
    $Site,

    # The screen for the icicle
    [Parameter(Mandatory=$true, ParameterSetName='Screen')]
    [Parameter(Mandatory=$true, ParameterSetName='UpdatedScreen')]
    [ScriptBlock]
    $Screen,

    # The command to use for the icicle.
    # The icicle will collect input for this command and run that command in the main runspace.
    [Parameter(Mandatory=$true, ParameterSetName='Command',ValueFromPipelineByPropertyName=$true, ValueFromPipeline=$true)]
    [Management.Automation.CommandMetaData]
    $Command,

    # A list of parameters to hide when displaying the command within an Icicle
    [Parameter(ParameterSetName='Command',ValueFromPipelineByPropertyName=$true)]
    [string[]]
    $HideParameter,


    # The command to use for the icicle.
    # The icicle will collect input for this command and run that command in the main runspace.
    [Parameter(Mandatory=$true, ParameterSetName='Module', ValueFromPipeline=$true)]
    [Management.Automation.PSModuleInfo]
    $Module,

    # The data update. This script will be run in the runspace that launched the icicle every UpdateEvery
    [Parameter(Mandatory=$true, ParameterSetName='UpdatedScreen')]
    [ScriptBlock]    
    $DataUpdate,

    # The UI update. This script will be run in the UI runspace, and can access the results from the dataupdate in $args
    [Parameter(Mandatory=$true, ParameterSetName='UpdatedScreen')]
    [ScriptBlock]
    $UiUpdate,
    
    # The frequency of the update.
    [Timespan]
    [Alias('UpdateFrequency')]
    [ValidateRange("00:00:01", "00:01:00")]
    $UpdateEvery,

    # If set, will not show the icicle when it is created.
    [Switch]
    $DoNotShow,

    # If set, the icicle will be horizontal.
    [Switch]
    $Horizontal,

    # If set, will remove an existing icicle before adding this one.
    [Switch]$Force,

    # If set, will make a shortcut key for the icicle. If the first key has a conflict, the next key will be used.
    [string[]]$ShortcutKey,

    # If set, will update the Icicle whenever the files change
    [switch]$UpdateOnFileChange,

    # If set, will update the Icicle whenever an add on is added or removed
    [switch]$UpdateOnAddOnChange
    )
            
    process { 

        #region Create Icicle
        $addonParams = @{
            DisplayName=$Name
            DoNotshow=$DoNotshow
            Force=$Force
        }
        if ($Horizontal) {
            $addonParams["AddHorizontally"] = $true
        } else {
            $addonParams["AddVertically"] = $true
        }
        if ($psCmdlet.ParameterSetName -like "*Screen*") {
            $addonParams.ScriptBlock= $screen
        } elseif ($pscmdlet.ParameterSetName -like "*Site*") {
        
            $addonParams.ScriptBlock = [ScriptBlock]::Create("
            New-WebBrowser -On_Loaded {
                `$fiComWebBrowser = `$this.GetType().GetField('_axIWebBrowser2', 'Instance,NonPublic')
                if (-not `$fiComWebBrowser) { return }
     
                `$objComWebBrowser = `$fiComWebBrowser.GetValue(`$this);
                if (-not `$objComWebBrowser) { return }
         
                 
                `$arr = new-Object Object[] 1
                `$arr[0] = `$true
                `$objComWebBrowser.GetType().InvokeMember('Silent', [Reflection.BindingFlags]'SetProperty', `$null, `$objComWebBrowser, `$arr)
 
            } -Source '$site'
            "
)
            if ($psCmdlet.ParameterSetName -like "*update*") {
                $uiUpdate = {
                    $this.Content.Source = $this.Content.Source
                } 
            }
        } elseif ($pscmdlet.ParameterSetName -eq 'Module') {
            $cmds = @($module.ExportedFunctions.Values) + @($module.ExportedCmdlets.Values)
            $moduleRoot = Split-Path $Module.Path
            if (Test-Path "$moduleRoot\$($module.Name).pipeworks.psd1") {
                # If there's a pipeworks manifest, create Icicles for all entries in WebCommand
                $pipeworksManifestContent = [IO.File]::ReadAllText("$moduleRoot\$($module.Name).pipeworks.psd1")
                $pipeworksManifest = "data { $([ScriptBlock]::Create($pipeworksManifestContent)) }" 
            } else {
                foreach ($cmd in $cmds) {
                    Add-Icicle -command $cmd -DoNotShow
                }
            }
            # Create a horizontal icicle to show each command

        } elseif ($psCmdlet.ParameterSetName -eq 'Command') {
            $psBoundParameters.DisplayName = $Command.Name
            $name = $command.Name    
            $safecommandName = $command.Name.Replace("-", "")
            $addonParams.DisplayName = $command.Name
            $addonParams.ScriptBlock = [ScriptBlock]::Create(@"
$input = . Get-WebInput -Control $this -CommandMetaData $cmds
New-Grid -Rows 1* -RoutedEvent @{
    [Windows.Controls.Button]::ClickEvent = {
         
        try {
            if (`$_.Source.Name -ne '$($command.Name.Replace("-", "") + "_Invoke")') {
                return
            }
            `$value = Get-ChildControl -Control `$this -OutputNamedControl
            foreach (`$kv in @(`$value.GetEnumerator())) {
                if ((`$kv.Key -notlike "${SafeCommandName}_*")) {
                    `$value.Remove(`$kv.Key)
                }
            }
 
            foreach (`$kv in @(`$value.GetEnumerator())) {
                if (`$kv.Value.Text) {
                    `$value[`$kv.Key] = `$kv.Value.Text
                } elseif (`$kv.Value.SelectedItems) {
                    `$value[`$kv.Key] = `$kv.Value.SelectedItems
                } elseif (`$kv.Value -is [Windows.Controls.Checkbox] -and `$kv.Value.IsChecked) {
                    `$value[`$kv.Key] = `$kv.Value.IsChecked
                } else {
                    `$value.Remove(`$kv.Key)
                }
            }
 
            foreach (`$kv in @(`$value.GetEnumerator())) {
                `$newKey = `$kv.Key.Replace("${SafeCommandName}_", "")
                `$newValue = `$kv.Value
                `$value.Remove(`$kv.Key)
                `$value.`$newKey = `$newValue
            }
 
            `$mainRunspace = [Windows.Window]::getWindow(`$this).Resources.MainRunspace
            if (`$value) {
                 
                 
                if (`$mainRunspace.RunspaceAvailability -ne 'Busy') {
                    `$mainRunspace.SessionStateProxy.SetVariable("IcicleCommandParameter", `$value)
                }
            }
             
            if (`$mainRunspace.RunspaceAvailability -ne 'Busy') {
                `$this.Parent.HostObject.CurrentPowerShellTab.Invoke({
                    if (`$IcicleCommandParameter ) {
                        $($command.Name) @IcicleCommandParameter
                    } else {
                        'Parameters Not Found'
                    }
                    #Remove-Variable IcicleCommandParameter
                })
            }
        } catch {
            [Windows.MessageBox]::Show("`$(`$_ | Out-String)", "Error")
        }
    }
} -ControlName '$($Command.Name)' -Children {
    [Windows.Markup.XamlReader]::Parse(@'
$(Request-CommandInput -CommandMetaData $command -Platform WPF)
'@)
}
"@
)
        }
        if ($Force -and (Get-Icicle $Name)) {
            Get-Icicle $Name | 
                Remove-Icicle -Confirm:$false
            
        }
        if ($shortcutKey) {
            $addonParams.shortcutKey  =$shortcutKey
        }
        ConvertTo-ISEAddOn @addonParams
        #endregion

        

        $processUiUpdate = {
    
        if ($horizontal) {
                $list = $psise.CurrentPowerShellTab.HorizontalAddOnTools   
        } else {
            $list = $psise.CurrentPowerShellTab.VerticalAddOnTools
        }

        $list | 
            Where-Object {
                $_.Name -eq $UpdateName
            } |
            ForEach-Object {                 
                $_.Control.InvokeScript($uiUpdate, @($outputValue))
            }
     
        }
        if (("$DataUpdate" -or "$UiUpdate") -and 
            $updateEvery.totalMilliseconds) { 
            $timer = 
                New-Object Timers.Timer -Property @{
                    Interval = $UpdateEvery.TotalMilliseconds
                }


            $fullaction = [ScriptBlock]::Create("
`$outputValue = & {
`$global:ProgressPreference = 'SilentlyContinue'
$dataupdate
`$global:ProgressPreference = 'Continue'
}
`$UiUpdate = {$UiUpdate}
`$horizontal = $(if ($horizontal) {'$true' } else { '$false' })
`$updateName = '$Name'
"
 + $processUiUpdate )

            #region Update Actions


        if ($UpdateOnToggleScriptView) {
            $tabSwitchAction = [ScriptBlock]::Create("" + {
                if ($EventArgs.PropertyName -notlike "*expand*") {
                    return
                }
                
            } + $fullAction)

            if ($force) {
                Get-EventSubscriber "${Name}IseScriptView" -ErrorAction SilentlyContinue | Unregister-Event
            }
            $null = 
                Register-ObjectEvent -SourceIdentifier "${Name}IseScriptView" -InputObject $psise.CurrentPowerShellTab -EventName PropertyChanged -Action $tabSwitchAction  

        }


        if ($UpdateOnAddOnChange) {
            if ($Force) {
                Get-EventSubscriber "${Name}IseVerticalAddOnsChanged" -ErrorAction SilentlyContinue | Unregister-Event
                Get-EventSubscriber "${Name}IseHorizontalAddOnsChanged" -ErrorAction SilentlyContinue | Unregister-Event
            }
            $null = 
                Register-ObjectEvent -SourceIdentifier "${Name}IseVerticalAddOnsChanged" -InputObject $psise.CurrentPowerShellTab.VerticalAddOnTools -EventName CollectionChanged -Action $fullAction

            $null = 
                Register-ObjectEvent -SourceIdentifier "${Name}IseHorizontalAddOnsChanged" -InputObject $psise.CurrentPowerShellTab.HorizontalAddOnTools -EventName CollectionChanged -Action $fullAction

        }

        if ($UpdateOnFileChange) {
            if ($Force) {
                Get-EventSubscriber "${Name}IseFilesChanged" -ErrorAction SilentlyContinue | Unregister-Event
            }
            $null = 
                Register-ObjectEvent -SourceIdentifier "${Name}IseFilesChanged" -InputObject $psise.CurrentPowerShellTab.Files -EventName CollectionChanged -Action $fullAction
            

        }

        $runSoon = New-Object Timers.Timer -Property @{
            AutoReset = $false 
            Interval = ([Timespan]"0:0:0.5").TotalMilliseconds
        }

        if ($Force) {
            Get-EventSubscriber "${Name}FirstUpdate" -ErrorAction SilentlyContinue | Unregister-Event
        }

        $null = 
            Register-ObjectEvent -SourceIdentifier "${Name}FirstUpdate" -InputObject $runSoon  -EventName Elapsed -Action $fullaction 
        $runsoon.Start()
        
        
        if ($fullAction -and $UpdateEvery.totalMilliseconds) { 
            

            # Old tricks from task scheduler: If everything has the exact same update interval, the program will seem to logjam
            # Therefore, randomly offset by 1/8th of a second to avoid some collisions
            $jitteredInterval = $UpdateEvery.TotalMilliseconds + (Get-Random -Maximum 250) - 125

            $timer = 
                New-Object Timers.Timer -Property @{
                    Interval = $jitteredInterval
                }


            
            #region Update Actions

            if ($Force) {
                Get-EventSubscriber "${Name}RegularUpdate" -ErrorAction SilentlyContinue | Unregister-Event
            }
            $null = 
                Register-ObjectEvent -SourceIdentifier "${Name}RegularUpdate" -InputObject $timer -EventName Elapsed -Action $fullaction 
            $timer.Start()

            # Run soon, so it "feels" right


            # Run when the users switches tabs
            #endregion

        }
    }
    $syncAction = [ScriptBlock]::Create(@"
 
`$outputValue = `$psise, ([Runspace]::DefaultRunspace)
`$horizontal = $(if ($horizontal) {'$true' } else { '$false' })
`$uiUpdate = {
     
    [Windows.Window]::getWindow(`$this).Resources.ISE = (`$args)[0]
    [Windows.Window]::getWindow(`$this).Resources.MainRunspace = (`$args)[1]
}
`$updateName = '$Name'
"@
 + $processUiUpdate )

        
    $SyncIse= New-Object Timers.Timer -Property @{
        Interval = ([Timespan]"0:0:2.$(Get-Random -Max 20)").TotalMilliseconds
    }
    if ($Force) {
                Get-EventSubscriber "${Name}SyncIse" -ErrorAction SilentlyContinue | Unregister-Event
            }
    $null = 
        Register-ObjectEvent -SourceIdentifier "${Name}SyncIse" -InputObject $SyncIse -EventName Elapsed -Action $syncAction 
    $SyncIse.Start()

}
}