PSChristmasTree.psm1

### --- PUBLIC FUNCTIONS --- ###
#region - Show-PSChristmasTree.ps1

function Show-PSChristmasTree() {
<#
 .Synopsis
  Display a christmas tree

 .Description
  Returns a christmas tree with decorations that lights up. It has many parameters to customize it

 .Parameter AnimationLoopNumber
  Number of times to loop animation

  .Parameter AnimationSpeed
  Time in milliseconds to show each frame

  .Parameter Colors
  All foreground colors possibilities you want to use (Array)

  .Parameter Decorations
  Hashtable :
      Key => character you want to animate
      Value => color you want to display this character

  .Parameter PlayCarol
  Number of times to loop "we wish you a merry christmas" carol

  .Parameter UICulture
  Set the language code in order to get it in locales, if it does not exist in locales, use the default one (en-US)

 .Example
   # Show christmas tree by playing "we wish you a merry christmas" carol once
   Show-PSChristmasTree -PlayCarol 1

 .LINK
    https://github.com/Sofiane-77/PSChristmasTree
#>

    [CmdletBinding()]
    [OutputType([System.void])]
    Param (
            [Parameter( Mandatory=$false, Position=0 )]
            [ValidateRange(1,[int]::MaxValue)]
            [int]$AnimationLoopNumber=50,

            [Parameter( Mandatory=$false, Position=1 )]
            [ValidateRange(1,[int]::MaxValue)]
            [int]$AnimationSpeed=300,

            [Parameter( Mandatory=$false, Position=2 )]
            [ValidateNotNullOrEmpty()]
            [array]$Colors=@('Black', 'DarkBlue', 'DarkGreen', 'DarkCyan', 'DarkRed', 'DarkMagenta', 'DarkYellow', 'Gray', 'DarkGray', 'Blue', 'Green', 'Cyan', 'Red', 'Magenta', 'Yellow', 'White'),

            [Parameter( Mandatory=$false, Position=3 )]
            [ValidateNotNullOrEmpty()]
            [hashtable]$Decorations=@{},

            [Parameter( Mandatory=$false, Position=4 )]
            [ValidateRange(0,[int]::MaxValue)]
            [int]$PlayCarol=0,

            [Parameter( Mandatory=$false, Position=5 )]
            [ValidateNotNullOrEmpty()]
            [string]$UICulture=(Get-UICulture).Name
    )

    BEGIN {
        # Save current value
        $CurrentColor = Get-ConsoleForegroundColor
        $CurrentBufferSize = Get-BufferSizeWidth
        $CurrentCursorSize = Get-CursorSize

        $ChristmasTree = Get-ChristmasTree
        $ChristmasDecorations = @{
            'O' = 'random';
            "$($ChristmasTree.trunk)" = 'red';
        } # See the comment for the decorations parameter
        $ChristmasDecorations = Merge-Hashtable $ChristmasDecorations $Decorations

        $ChristmasTree.tree = Get-CenteredText -Text $ChristmasTree.tree # Center in terminal
        $ChristmasTree.tree = Add-DecorationTag -Text $ChristmasTree.tree -Decorations $ChristmasDecorations

        $Messages = Import-LocalizedData -BaseDirectory (Join-Path -Path $PSScriptRoot -ChildPath "./locales") -FileName "Messages.psd1" -UICulture $UICulture    -ErrorAction:SilentlyContinue
        $Messages.MerryChristmas.Text = $Messages.MerryChristmas.Text.ToUpper();
        $Messages.MessageForDevelopers.Text = $Messages.MessageForDevelopers.Text.replace("{1}", (Get-NewYear));
        $Messages.HappyNewYear.Text = $Messages.HappyNewYear.Text.ToUpper();

        Hide-CursorSize
    }

    PROCESS {
        Invoke-Carol $PlayCarol

        $i = 0
        do {

            if ($CurrentBufferSize -ine (Get-BufferSizeWidth)) {

                $ChristmasTree.tree = Get-CenteredText -Text (Get-ChristmasTree).tree # Re-center the original tree

                $ChristmasTree.tree = Add-DecorationTag -Text $ChristmasTree.tree -Decorations $ChristmasDecorations

                $CurrentBufferSize = Get-BufferSizeWidth

                Hide-CursorSize
            }

            Clear-Host

            Write-Host-Colorized -DecoratedText $ChristmasTree.tree -Colors $Colors -DefaultForegroundColor 'Green' # Color of the chrismas tree

            Write-Host (Get-CenteredText -Text $Messages.MerryChristmas.Text) -ForegroundColor ($Messages.MerryChristmas.Colors | Get-Random)

            Write-Host-Colorized (Get-DecoratedFormattedText -FormattedText $Messages.MessageForDevelopers.Text -FormattedValue $Messages.MessageForDevelopers.'{0}' -Centered $true) -Colors $Colors -DefaultForegroundColor $Messages.MessageForDevelopers.Color

            Write-Host (Get-CenteredText -Text $Messages.HappyNewYear.Text) -ForegroundColor ($Messages.HappyNewYear.Colors | Get-Random)

            Start-Sleep -Milliseconds $AnimationSpeed

            $i++

        } until ($i -eq $AnimationLoopNumber)
    }

    END {
        # We dont need to set CursorSize if the value is null
        if (![string]::IsNullOrWhitespace($CurrentCursorSize)) {
            Set-CursorSize $CurrentCursorSize
        }

        Set-ConsoleForegroundColor $CurrentColor
    }
}
Export-ModuleMember -Function Show-PSChristmasTree
#endregion
### --- PRIVATE FUNCTIONS --- ###
#region - Add-DecorationTag.ps1

function Add-DecorationTag() {
<#
 .Synopsis
  Decorate Text with Tags to be able to add color to specific character

 .Description
  Replace character with #color#character# pattern

 .Parameter Text
  Text to decorate

 .Parameter Decorations
  Hashtable :
      Key => character you want to animate
      Value => color you want to display this character

 .Example
   # Decorate "Hello World!" for "o" character to be red
   Add-DecorationTag "Hello World!" @{'o'='red'}
   Result : Hell#red#o# W#red#o#rld!
#>

    [CmdletBinding()]
    [OutputType([String])]
    Param (
            [Parameter( Mandatory = $true, Position=0 )]
            [ValidateNotNullOrEmpty()]
            [string]$Text,

            [Parameter( Mandatory = $true, Position=1 )]
            [ValidateNotNullOrEmpty()]
            [Hashtable]$Decorations
    )

    BEGIN {}

    PROCESS {
        foreach ($decoration in $Decorations.keys) {
            $Text = $Text.Replace("$decoration", "#$($Decorations.$decoration)#$decoration#")
        }
    }

    END {
        return $Text
    }
}
#endregion
#region - Get-BufferSizeWidth.ps1

function Get-BufferSizeWidth() {
<#
 .Synopsis
  Buffer size width of terminal

 .Description
  Return BufferSize of terminal
#>

    return $Host.UI.RawUI.BufferSize.Width
}
#endregion
#region - Get-CenteredText.ps1

function Get-CenteredText() {
<#
 .Synopsis
  Center a text in console

 .Description
  Returns the text with amount of space needed to be centered on each line

 .Parameter Text
  Text to center

 .Example
   # Center "Hello World!".
   Center-Text "Hello World!"
#>

    [CmdletBinding()]
    [OutputType([String])]
    Param (
            [Parameter( Mandatory = $true, Position=0, ValueFromPipeline=$true )]
            [ValidateNotNullOrEmpty()]
            [string]$Text
    )

    BEGIN {
        $CenteredString = [System.Collections.ArrayList]::new() # Array will contains each line centered
    }

    PROCESS {
        foreach ($line in $Text -split [System.Environment]::NewLine) {
            $line = $line.Trim()
            [void]$CenteredString.Add(("{0}{1}" -f (' ' * (([Math]::Max(0, $Host.UI.RawUI.BufferSize.Width / 2) - [Math]::Floor($line.Length / 2)))), $line))
        }
    }

    END {
        return $CenteredString -Join [System.Environment]::NewLine # Join each line to return a string
    }
}
#endregion
#region - Get-ChristmasTree.ps1

function Get-ChristmasTree() {
<#
 .Synopsis
  Christmas tree

 .Description
  Return hashtable with christmas tree and trunk
#>

    return @{
    'tree' = @"
         |
        -+-
         A
        /=\
      i/ O \i
      /=====\
      / i \
    i/ O * O \i
    /=========\
    / * * \
  i/ O i O \i
  /=============\
  / O i O \
i/ * O O * \i
/=================\
       |___|
"@
;
    'trunk' = '|___|'; # Trunk of christmas tree
    }
}
#endregion
#region - Get-ConsoleForegroundColor.ps1

function Get-ConsoleForegroundColor() {
<#
 .Synopsis
  Foreground color of console

 .Description
  Foreground color of console
#>

    return [Console]::ForegroundColor
}
#endregion
#region - Get-CursorSize.ps1

function Get-CursorSize() {
<#
 .Synopsis
  Cursor size

 .Description
  Return cursor size of terminal
#>

    # we tried [Console]::CursorSize but it make exception
    return $HOST.UI.RawUI.CursorSize
}
#endregion
#region - Get-DecoratedFormattedText.ps1

function Get-DecoratedFormattedText() {
<#
 .Synopsis
  Decorate formatted text

 .Description
  Returns decorated formatted text

 .Parameter FormattedText
  Formatted Text

 .Parameter FormattedValue
  Value to put inside FormattedText

 .Parameter Centered
  Center Text

 .Example
   # Add decoration for World
   Get-DecoratedFormattedText -FormattedText "Hello {0}!" -FormattedValue "World" -Centered $false
   Result : Hello #random#World#!
#>

    [CmdletBinding()]
    [OutputType([String])]
    Param (
            [Parameter( Mandatory = $true, Position=0 )]
            [ValidateNotNullOrEmpty()]
            [string]$FormattedText,

            [Parameter( Mandatory = $true, Position=1 )]
            [ValidateNotNullOrEmpty()]
            [string]$FormattedValue,

            [Parameter( Mandatory = $false, Position=2 )]
            [boolean]$Centered=$false
    )

    BEGIN {
        $Text = ""

        if ($Centered) {
            $FormattedText = (Get-CenteredText -Text ($FormattedText -f $FormattedValue)).Split($FormattedText[0])[0] + $FormattedText
        }
    }

    PROCESS {

        $decorations = @{
            "$FormattedValue" = 'random'
        }

        $Text = Add-DecorationTag -Text $FormattedValue -Decorations $decorations
    }

    END {
        return $FormattedText -f $Text
    }
}
#endregion
#region - Get-NewYear.ps1

function Get-NewYear() {
<#
 .Synopsis
  Get Year for christmas messages

 .Description
  Returns the current year or new year depends on when you call it
#>

    [OutputType([int])]
    [int]$currentYear = Get-Date -UFormat "%Y"

    # The following year is displayed only if the date is greater than December 23
    if ((Get-Date) -gt (Get-Date -Year $currentYear -Month 12 -Day 23)) {
        $newYear = $currentYear + 1
        return $newYear
    }
    return $currentYear
}
#endregion
#region - Hide-CursorSize.ps1

function Hide-CursorSize() {
<#
 .Synopsis
  Hide cursor size of terminal

 .Description
  Hide cursor size of terminal

 .Example
   # Hide cursor size
   Hide-CursorSize
#>

    return Set-CursorSize 1 # Hide the cursor before display the tree
}
#endregion
#region - Invoke-Carol.ps1

function Invoke-Carol() {
<#
 .Synopsis
  Play "we wish you a merry christmas"

 .Description
  Play the carol in antoher thread

 .Parameter CarolLoopNumber
  Number of carol repetitions

 .Example
   # Play the carol 1 time
   Invoke-Carol 1
#>

    [CmdletBinding()]
    [OutputType([System.Void])]
    Param (
            [Parameter( Mandatory = $false, Position=0 )]
            [ValidateRange(0,[int]::MaxValue)]
            [int]$CarolLoopNumber=1
    )

    BEGIN {
        # We created another thread to be able to play the song AND display the tree
        $Runspace = [runspacefactory]::CreateRunspace()
        $PowerShell = [powershell]::Create()
        $PowerShell.Runspace = $Runspace
        $Runspace.Open()
        $Runspace.SessionStateProxy.SetVariable('CarolLoopNumber', $CarolLoopNumber) # Share parameter to other powershell
    }

    PROCESS {
        $PowerShell.AddScript({

            For($i=0;$i -lt $CarolLoopNumber;$i++)
            {
                $Duration = @{
                    WHOLE = 1600;
                }
                $Duration.HALF = $Duration.WHOLE/2;
                $Duration.QUARTER = $Duration.HALF/2;
                $Duration.EIGHTH = $Duration.QUARTER/2;
                $Duration.SIXTEENTH = $Duration.EIGHTH/2;

                # Hashtable where key = note and value = frequency
                $Notes = @{
                    A4 = 440;
                    B4 = 493.883301256124;
                    C5 = 523.251130601197;
                    D5 = 587.329535834815;
                    E5 = 659.25511382574;
                    F5 = 698.456462866008;
                    G4 = 391.995435981749;
                }


                [console]::beep($Notes.G4,$Duration.EIGHTH)

                Start-Sleep -m $Duration.SIXTEENTH

                [console]::beep($Notes.C5,$Duration.EIGHTH)

                Start-Sleep -m $Duration.SIXTEENTH

                [console]::beep($Notes.C5,$Duration.EIGHTH)

                [console]::beep($Notes.D5,$Duration.EIGHTH)

                [console]::beep($Notes.C5,$Duration.EIGHTH)

                [console]::beep($Notes.B4,$Duration.EIGHTH)

                [console]::beep($Notes.A4,$Duration.EIGHTH)

                Start-Sleep -m $Duration.SIXTEENTH

                [console]::beep($Notes.A4,$Duration.EIGHTH)

                Start-Sleep -m $Duration.SIXTEENTH

                [console]::beep($Notes.A4,$Duration.EIGHTH)

                Start-Sleep -m $Duration.SIXTEENTH

                [console]::beep($Notes.D5,$Duration.EIGHTH)

                Start-Sleep -m $Duration.SIXTEENTH

                [console]::beep($Notes.D5,$Duration.EIGHTH)

                [console]::beep($Notes.E5,$Duration.EIGHTH)

                [console]::beep($Notes.D5,$Duration.EIGHTH)

                [console]::beep($Notes.C5,$Duration.EIGHTH)

                [console]::beep($Notes.B4,$Duration.EIGHTH)

                Start-Sleep -m $Duration.SIXTEENTH

                [console]::beep($Notes.G4,$Duration.EIGHTH)

                Start-Sleep -m $Duration.SIXTEENTH

                [console]::beep($Notes.G4,$Duration.EIGHTH)

                Start-Sleep -m $Duration.SIXTEENTH

                [console]::beep($Notes.E5,$Duration.EIGHTH)

                Start-Sleep -m $Duration.SIXTEENTH

                [console]::beep($Notes.E5,$Duration.EIGHTH)

                [console]::beep($Notes.F5,$Duration.EIGHTH)

                [console]::beep($Notes.E5,$Duration.EIGHTH)

                [console]::beep($Notes.D5,$Duration.EIGHTH)

                [console]::beep($Notes.C5,$Duration.EIGHTH)

                Start-Sleep -m $Duration.SIXTEENTH

                [console]::beep($Notes.A4,$Duration.EIGHTH)

                Start-Sleep -m $Duration.SIXTEENTH

                [console]::beep($Notes.G4,$Duration.EIGHTH)

                Start-Sleep -m $Duration.SIXTEENTH

                [console]::beep($Notes.A4,$Duration.EIGHTH)

                Start-Sleep -m $Duration.SIXTEENTH

                [console]::beep($Notes.D5,$Duration.EIGHTH)

                Start-Sleep -m $Duration.SIXTEENTH

                [console]::beep($Notes.B4,$Duration.EIGHTH)

                Start-Sleep -m $Duration.SIXTEENTH

                [console]::beep($Notes.C5,$Duration.EIGHTH)
            }
        })

        $PowerShell.BeginInvoke()
    }

    END {}
}
#endregion
#region - Merge-Hashtable.ps1

Function Merge-Hashtable() {
<#
 .Synopsis
  Merge Hashtable

 .Description
  Return a merged Hashtable
#>

    [OutputType([Hashtable])]
    $Output = @{}
    foreach ($Hashtable in $Args) {
        If ($Hashtable -is [Hashtable]) {
            foreach ($Key in $Hashtable.Keys) {$Output.$Key = $Hashtable.$Key}
        }
    }
    return $Output
}
#endregion
#region - Remove-DecorationTag.ps1

function Remove-DecorationTag() {
<#
 .Synopsis
  Remove Tags for decorated Text

 .Description
  Replace #color#character# pattern with character

 .Parameter DecoratedText
  Decorated Text with Add-DecorationTags

 .Parameter Decorations
    Hashtable :
      Key => character you have chosen to animate
      Value => color you have chosen to display this character

 .Example
   # Remove tags for decorated Text
   Remove-DecorationTag "Hell#red#o# W#red#o#rld!" @{'o'='red'}
   Result : Hello World!
#>

    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([String])]
    Param (
            [Parameter( Mandatory = $true, Position=0 )]
            [ValidateNotNullOrEmpty()]
            [string]$DecoratedText,

            [Parameter( Mandatory = $true, Position=1 )]
            [ValidateNotNullOrEmpty()]
            [Hashtable]$Decorations
    )

    BEGIN {
        $Text = ""
    }

    PROCESS {
        if($PSCmdlet.ShouldProcess($DecoratedText)){
            foreach ($decoration in $Decorations.keys) {
                $Text = $DecoratedText.Replace("#$($Decorations.$decoration)#$decoration#", "$decoration")
            }
        }
    }

    END {
        return $Text
    }
}
#endregion
#region - Set-ConsoleForegroundColor.ps1

function Set-ConsoleForegroundColor() {
<#
 .Synopsis
  Set foreground color of console

 .Description
  Set foreground color of console

 .Parameter Color
  new value of console foreground color

 .Example
   # Set foreground color to gray
   Set-ConsoleForegroundColor "Gray"
#>

    [CmdletBinding(SupportsShouldProcess)]
    Param (
            [Parameter( Mandatory = $true, Position=0 )]
            [ValidateNotNullOrEmpty()]
            [System.ConsoleColor]$Color
    )

    BEGIN {}

    PROCESS {
        if($PSCmdlet.ShouldProcess('[Console]::ForegroundColor)')){
            [Console]::ForegroundColor = $Color
        }
    }

    END {}
}
#endregion
#region - Set-CursorSize.ps1

function Set-CursorSize() {
<#
 .Synopsis
  Set cursor size of terminal

 .Description
  Set cursor size of terminal

 .Parameter Size
  new value of cursor size

 .Example
   # Set cursor size to 10
   Set-CursorSize 10
#>

    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([Bool])]
    Param (
            [Parameter( Mandatory = $true, Position=0 )]
            [ValidateRange(1,100)]
            [int]$Size
    )

    BEGIN {}

    PROCESS {
        if($PSCmdlet.ShouldProcess('[Console]::CursorSize')){
            try{
                [Console]::CursorSize = $Size
                return $true
            }
            catch [System.PlatformNotSupportedException],[System.Management.Automation.SetValueInvocationException] {
                # The cursor size don't have setter on linux and macos
                return $false
            }
        }
    }

    END {}
}
#endregion
#region - Write-Host-Colorized.ps1

function Write-Host-Colorized() {
<#
 .Synopsis
  Write-Host with color

 .Description
  Display Text with color on host

 .Parameter DecoratedText
  Decorated Text with Add-DecorationTags

 .Parameter Colors
  List of all desired colors

 .Parameter DefaultForegroundColor
  Default text color when no color or decoration is specified

 .Example
   # Display "Hello World!" in white with red o
   Write-Host-Colorized "Hell#red#o# W#red#o#rld!" @('Red') "white"
#>

    [CmdletBinding()]
    [OutputType([System.Void])]
    Param (
            [Parameter( Mandatory = $true, Position=0 )]
            [ValidateNotNullOrEmpty()]
            [string]$DecoratedText,

            [Parameter( Mandatory = $true, Position=1 )]
            [ValidateNotNullOrEmpty()]
            [Array]$Colors,

            [Parameter( Mandatory = $true, Position=2)]
            [ValidateNotNullOrEmpty()]
            [string]$DefaultForegroundColor
    )

    BEGIN {
        $currentColor = $DefaultForegroundColor
        $allColors = $Colors + 'Random' # Add random to the list of possible colors
    }

    PROCESS {
        # Iterate through splitted Messages
        foreach ( $string in $DecoratedText.split('#') ){
            # If a string between #-Tags is equal to any predefined color, and is equal to the defaultcolor: set current color
            if ( $allColors -contains $string -and $currentColor -eq $DefaultForegroundColor ){
                # if random chosen, we need to set a real color
                if ($string -ieq 'random') {
                    $string = ($Colors | Get-Random)
                }
                $currentColor = $string
            }else{
                # If string is a output message, than write string with current color (with no line break)
                Write-Host -nonewline -f $currentColor "$string"
                # Reset current color
                $currentColor = $DefaultForegroundColor
            }
        }
    }

    END {
        # Single write-host for the final line break
        Write-Host
    }
}
#endregion