EzTheme.psm1

using module @{ModuleName = "Configuration";    ModuleVersion = "1.4.0"}
using namespace System.Collections.Generic
using namespace System.Management.Automation
#Region '.\Classes\10 ThemeId.ps1' 0
class ThemeId : IPathInfo {
    [string]$Name
    [string]$Path

    ThemeId ([string]$Path) {
        $this.Path = Convert-Path $Path
        $this.Name = [IO.Path]::GetFileName($this.Path) -replace "\.theme\.psd1$"
    }

    [string]ToString() {
        return $this.Name
    }


    # Explicitly implement interface for PS5x
    [string] get_Name() {
        return $this.Name
    }
    [void] set_Name([string]$value) {
        $this.Name = $value
    }
    [string] get_Path() {
        return $this.Path
    }
    [void] set_Path([string]$value) {
        $this.Path = $value
    }
}
#EndRegion '.\Classes\10 ThemeId.ps1' 29
#Region '.\Classes\20 Theme.ps1' 0
#using module @{ModuleName = "Configuration"; ModuleVersion = "1.4.0"}
#using namespace System.Collections.Generic

class Theme : ITheme, IPsMetadataSerializable {
    [string]$Name
    [string]$Path
    [Dictionary[string, PSObject]]$Settings = [Dictionary[string, PSObject]]::new()

    [System.Management.Automation.HiddenAttribute()]
    [void] LoadTheme() {
        $Theme = Import-Metadata $this.Path -ErrorAction Stop
        foreach ($key in $Theme.Keys) {
            $null = $this.Settings.Add($key, $Theme[$key])
        }
    }

    Theme ([string]$Path) {
        $this.Path = Convert-Path $Path
        $this.Name = [IO.Path]::GetFileName($this.Path) -replace "\.theme\.psd1$"
        $this.LoadTheme()
    }

    Theme ([ThemeId]$Path) {
        $this.Path = $Path.Path
        $this.Name = $Path.Name
        $this.LoadTheme()
    }

    [string[]] get_Modules() {
        return @($this.Settings.Keys)
    }
    [string[]] FindModules([string]$Module) {
        return $this.Settings.Keys.Where({
            ($_ -eq $Module) -or ($_ -like $Module) -or $_ -eq "Theme.$Module" -or $_ -eq "$Module.Theme"
        })
    }

    [object] get_Item([string]$Module) {
        if (!$this.Settings.ContainsKey($Module)) {
            [string[]]$Module = $this.Settings.Keys.Where({
                        ($_ -like $Module) -or $_ -eq "Theme.$Module" -or $_ -eq "$Module.Theme"
                      })
        }
        return $this.Settings[$Module]
    }

    [void] set_Item([string]$Module, [object]$Value) {
        $this.Settings[$Module] = $Value
    }

    [void] Remove([string]$ModuleName) {
        $this.Settings.Remove($ModuleName)
    }

    [string]ToString() {
        return $this.Name
    }

    # Serialization constructor
    Theme() {}

    [string]ToPsMetadata() {
        return ConvertTo-Metadata -InputObject @{
            Name = $this.Name
            Settings = $this.Settings
        }
    }
    [void] FromPsMetadata([string]$Metadata) {
        $Theme = ConvertFrom-Metadata -InputObject $Metadata
        $this.Name = $Theme.Name
        foreach ($key in $Theme.Setting.Keys) {
            $null = $this.Settings.Add($key, $Theme.Settings[$key])
        }
    }


    # Explicitly implement interface for PS5x
    [string] get_Name() {
        return $this.Name
    }
    [void] set_Name([string]$value) {
        $this.Name = $value
    }
    [string] get_Path() {
        return $this.Path
    }
    [void] set_Path([string]$value) {
        $this.Path = $value
    }
}

# Add-MetadataConverter @{ [Theme] = { "'$_'" } }
#EndRegion '.\Classes\20 Theme.ps1' 93
#Region '.\Private\ExportTheme.ps1' 0
function ExportTheme {
    <#
        .SYNOPSIS
            Exports a theme to a file
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        # The name of the theme
        [Parameter(Position = 0, Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Name,

        [Parameter(ValueFromPipeline, Position = 1)]
        $InputObject,

        [switch]$Force,

        [switch]$PassThru
    )
    process {
        $Name = $Name -replace "((\.theme)?\.psd1)?$"
        $NativeThemePath = Join-Path $(Get-ConfigurationPath -Scope "User") "$Name.theme.psd1"

        if (Test-Path -LiteralPath $NativeThemePath) {
            if($Force -or $PSCmdlet.ShouldContinue("Overwrite $($NativeThemePath)?", "$Name Theme exists")) {
                Write-Verbose "Exporting to $NativeThemePath"
                $InputObject | Export-Metadata $NativeThemePath
            }
        } else {
            Write-Verbose "Exporting to $NativeThemePath"
            $InputObject | Export-Metadata $NativeThemePath
        }

        if($PassThru) {
            $InputObject | Add-Member NoteProperty Name $Name -Passthru |
                           Add-Member NoteProperty Path $NativeThemePath -Passthru
        }

        @{ Theme = $Name } | Export-Configuration
    }
}
#EndRegion '.\Private\ExportTheme.ps1' 42
#Region '.\Private\FindTheme.ps1' 0
function FindTheme {
    <#
        .SYNOPSIS
            Finds themes in the file system
        .DESCRIPTION
            List available theme names with full PSPath
    #>

    [CmdletBinding()]
    param(
        # The name of the theme(s) to find. Supports wildcards, and defaults to * everything.
        [string]$Name = "*"
    )
    process {
        $Name = $Name -replace "((\.theme)?\.psd1)?$" -replace '$', ".theme.psd1"
        [ThemeId[]]@(
            Join-Path $(
                Get-ConfigurationPath -Scope User -SkipCreatingFolder
                Join-Path $PSScriptRoot Themes
            ) -ChildPath $Name -Resolve -ErrorAction Ignore)
    }
}
#EndRegion '.\Private\FindTheme.ps1' 22
#Region '.\Private\ImportTheme.ps1' 0
function ImportTheme {
    <#
        .SYNOPSIS
            Imports themes by name
    #>

    [CmdletBinding()]
    param(
        # A theme to import (can be the name of an installed theme, or the full path to a psd1 file)
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)]
        [Alias("PSPath")]
        [string]$Name,

        # One or more modules to export the theme from (ignores all other modules)
        [Parameter(ParameterSetName = "Whitelist")]
        [Alias("Module")]
        [string[]]$IncludeModule,

        # One or more modules to skip in the theme
        [Parameter(Mandatory, ParameterSetName = "Blacklist")]
        [string[]]$ExcludeModule
    )
    process {
        # Trace-Message -Verbose "PROCESS ImportTheme $Name $($PSCmdlet.ParameterSetName)"
        Write-Verbose "Loading theme from disk: $Name"

        # Normalize the Path, the file name must end with ".theme.psd1"
        $FileName = $Name -replace "((\.theme)?\.psd1)?$" -replace '$', ".theme.psd1"

        # The path needs to be a full file-sytem path
        if (Test-Path -LiteralPath $FileName) {
            $Path = Convert-Path -LiteralPath $FileName
        }

        # Trace-Message -Verbose "PATH: $Path"
        # Otherwise, use FindTheme
        if (!$Path) {
            $Themes = @(FindTheme $Name)
            if ($Themes.Count -eq 1) {
                $Path = $Themes[0].Path
            } elseif ($Themes.Count -gt 1) {
                Write-Warning "No exact match for $Name. Using $($Themes[0]), but also found $($Themes[1..$($Themes.Count-1)] -join ', ')"
                $Path = $Themes[0].Path
            }
            if (!$Path) {
                Write-Error "No theme '$Name' found. Try Get-Theme to see available themes."
                return
            }
        }

        Write-Verbose "Importing $Name theme from $Path"
        # Trace-Message -Verbose "Importing by casting [Theme]$Path"
        $Theme = [Theme]$Path

        if ($IncludeModule) {
            # Trace-Message "Filter IncludeModule $IncludeModule"
            Write-Debug "IncludeModule: $IncludeModule"
            $IncludeModule = @(
                foreach ($module in $IncludeModule) {
                    $Theme.Modules.Where{ ($_ -like $Module) -or $_ -eq "Theme.$Module" -or $_ -eq "$Module.Theme" }
                }
            )
            Write-Debug "IncludeModule: $IncludeModule"
            foreach ($unwanted in $Theme.Modules.Where{ $_ -notin $IncludeModule }) {
                Write-Debug "Removing $Unwanted from imported $Name theme"
                $null = $Theme.Remove($unwanted)
            }
        } elseif ($ExcludeModule) {
            # Trace-Messsage "Filter ExcludeModule $ExcludeModule"
            Write-Debug "ExcludeModule: $ExcludeModule"
            foreach ($module in $ExcludeModule) {
                foreach ($unwanted in @($Theme.Modules.Where{ ($_ -like $Module) -or $_ -eq "Theme.$Module" -or $_ -eq "$Module.Theme" })) {
                    Write-Debug "Excluding $Unwanted from imported $Name theme"
                    $null = $Theme.Remove($unwanted)
                }
            }
        }
        # Trace-Message -Verbose "END ImportTheme"

        $Theme
    }
}
#EndRegion '.\Private\ImportTheme.ps1' 82
#Region '.\Public\Export-Theme.ps1' 0
function Export-Theme {
    <#
        .SYNOPSIS
            Exports the current settings as a theme.
        .DESCRIPTION
            Exports the current theme settings from all currently loaded modules (or from a specified list of modules)
    #>

    [Alias("epth")]
    [CmdletBinding(DefaultParameterSetName = "Default")]
    param(
        # The name of the theme to export the current settings
        [Parameter(Position = 0, Mandatory)]
        [ValidateNotNullOrEmpty()]
        [ArgumentCompleter({
            param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
            Get-Theme $wordToComplete*
        })]
        [string]$Name,

        # One or more modules to export the theme from (ignores other modules)
        [Parameter()]
        [string[]]$Module,

        # If set, leave any additional modules in the theme
        [Parameter(ParameterSetName = "Default")]
        [switch]$Update,

        # If set, overwrite the existing theme
        [Parameter(ParameterSetName = "Overwrite")]
        [switch]$Force,

        # If set, pass through the theme object after exporting it to file
        [switch]$Passthru
    )
    end {
        $Theme = if ($Update) {
            ImportTheme $Name
        } else {
            @{}
        }

        $ModuleInfo = if (!$Module) {
            @(Get-Module).Where{ $_.PrivateData -and $_.PrivateData.ContainsKey("EzTheme") }
        } else {
            Get-Module $Module
        }

        foreach ($mi in $ModuleInfo) {
            Write-Verbose "Get theme from $($mi.Name)"
            try {
                $Theme[$mi.Name] = & "$($mi.Name)\$($mi.PrivateData["EzTheme"]["Get"])"
            } catch {
                Write-Warning "Unable to get theme from $($mi.Name)\$($mi.PrivateData["EzTheme"]["Get"])"
            }
        }

        $Theme | ExportTheme -Name $Name -Passthru:$Passthru -Force:($Force -or $Update)
        $MyInvocation.MyCommand.Module.PrivateData["Theme"] = $Theme
    }
}
#EndRegion '.\Public\Export-Theme.ps1' 61
#Region '.\Public\Get-ModuleTheme.ps1' 0
#using namespace System.Management.Automation
function Get-ModuleTheme {
    <#
        .Synopsis
            Get's the current theme information for a specific module
        .Description
 
        .Example
            Get-ModuleTheme | Set-MyModuleTheme
 
            This is how you should call it from the bottom of your MyModule module
        .Example
            Get-Module MyModule | Get-ModuleTheme
 
            You can see the current theme configuration for a particular module
        .Example
            Get-ModuleTheme Darkly -Module MyModule
 
            You can see the current theme configuration for a particular module
    #>

    [Alias("gmth")]
    [CmdletBinding(DefaultParameterSetName = '__CallStack')]
    param(
        [Parameter(Position = 0)]
        [string]$Name,

        # The name of the module you want to fetch theme data for
        [Parameter(ParameterSetName = "__ModuleName", Mandatory, ValueFromPipelineByPropertyName = $true)]
        [string]$Module,

        # The Module you want to fetch theme data for
        [Parameter(ParameterSetName = "__ModuleInfo", Mandatory, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [System.Management.Automation.PSModuleInfo]$InputObject,

        # The callstack. You should not ever pass this.
        # It is used to calculate the defaults for all the other parameters.
        [Parameter(ParameterSetName = "__CallStack")][Hidden()]
        [System.Management.Automation.CallStackFrame[]]${_ _ CallStack} = $(Get-PSCallStack)
    )
    process {
        if ($PSCmdlet.ParameterSetName -eq "__CallStack") {
            [System.Management.Automation.PSModuleInfo]$InputObject = . {
                $Command = (${_ _ CallStack})[0].InvocationInfo.MyCommand
                $mi = if ($Command.ScriptBlock -and $Command.ScriptBlock.Module) {
                    $Command.ScriptBlock.Module
                } else {
                    $Command.Module
                }

                if ($mi -and $mi.ExportedCommands.Count -eq 0) {
                    if ($mi2 = Get-Module $mi.ModuleBase -ListAvailable | Where-Object { ($_.Name -eq $mi.Name) -and $_.ExportedCommands } | Select-Object -First 1) {
                        $mi = $mi2
                    }
                }
                $mi
            }
        }
        if ($InputObject) {
            $Module = $InputObject.Name
        }
        if ($Module) {
            $Theme = if ($Name) {
                ImportTheme $Name
            } else {
                $MyInvocation.MyCommand.Module.PrivateData["Theme"]
            }
            if ($Theme) {
                $Theme.Item($Module)
            }
        }
    }
}
#EndRegion '.\Public\Get-ModuleTheme.ps1' 73
#Region '.\Public\Get-Theme.ps1' 0
function Get-Theme {
    <#
        .SYNOPSIS
            List available themes, optionally filtering
        .DESCRIPTION
            List available themes, optionally filtering by partial name or functionality.
    #>

    [Alias("gth")]
    [CmdletBinding()]
    param(
        # The name of the theme(s) to show. Supports wildcards, and defaults to * everything.
        [Alias("Theme", "PSPath")]
        [Parameter(ValueFromPipelineByPropertyName, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [ArgumentCompleter({
            param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
            Get-Theme $wordToComplete*
        })]
        [string]$Name = "*",

        # If set, only returns themes that support theming all the specified modules. Supports wildcards.
        [Alias("Module")]
        [AllowEmptyCollection()]
        [string[]]$SupportedModule,

        # If set, outputs just the module theme object(s)
        [string[]]$ExpandModule
    )
    Write-Verbose "Searching for theme: $Name"
    $Themes = @(FindTheme $Name)
    foreach ($Theme in $Themes) {
        if ($SupportedModule -or $ExpandModule -or ($Themes.Count -eq 1)) {
            $ThemeData = [Theme]$Theme

            Write-Verbose "The $($ThemeData.Name) theme supports $($ThemeData.Modules -join ', ')"

            $SupportedModule.ForEach({
                $ExpectedModule = $_
                if (!$ThemeData.Item($_)) {
                    # skip outputting this theme because it doesn't support this module
                    Write-Verbose "The $Name theme doesn't support $ExpectedModule $($ThemeData.Modules -join ', ')"
                    continue # goes to the outer foreach in FindTheme
                }
            })
            if ($ExpandModule) {
                $ThemeData.Item($ExpandModule) | ForEach-Object { $_ } # Enumerate
            } else {
                $ThemeData
            }
        } else {
            $Theme
        }
    }
}
#EndRegion '.\Public\Get-Theme.ps1' 55
#Region '.\Public\Import-Theme.ps1' 0
function Import-Theme {
    <#
        .SYNOPSIS
            Import a named theme and apply it to all the registered themable modules that are in the theme
        .EXAMPLE
            Import-Theme Light
 
            Imports the built-in Light theme and applies it to all the supported modules that are registered
        .EXAMPLE
            Import-Theme Light -Include Theme.ConsoleColors
 
            Imports the built-in Light theme, but only applies the theme to Theme.ConsoleColors
    #>

    [Alias("ipth")]
    [CmdletBinding(DefaultParameterSetName = "Whitelist")]
    param(
        # A theme to import (can be the name of an installed theme, or the full path to a psd1 file)
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)]
        [ArgumentCompleter({
            param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
            Get-Theme $wordToComplete*
        })]
        [string]$Name,

        # One or more modules to export the theme from (ignores all other modules)
        [Parameter(ParameterSetName = "Whitelist")]
        [Alias("Module")]
        [string[]]$IncludeModule,

        # One or more modules to skip in the theme
        [Parameter(Mandatory, ParameterSetName = "Blacklist")]
        [string[]]$ExcludeModule,

        # Normally, only modules that are currently imported are themed.
        # If you set this, any module in the theme that's installed will be imported and themed
        [switch]$Force
    )
    begin {
        # Trace-Message -Verbose "BEGIN Import-Theme $Name"
        $SupportedModules = @(Get-Module).Where{ $_.PrivateData -is [Collections.IDictionary] -and $_.PrivateData.ContainsKey("EzTheme") }
        # Trace-Message -Verbose "Found $($SupportedModules.Count) modules"
        if (!$IncludeModule) {
            $IncludeModule = $SupportedModules
        }
    }
    process {
        # Trace-Message -Verbose "PROCESS Import-Theme $Name -IncludeModule $IncludeModule"
        $null = $PSBoundParameters.Remove("Force")
        $Theme = ImportTheme @PSBoundParameters
        # Trace-Message -Verbose "Setting EzTheme.PrivateData.Theme $($Theme.Name)"
        # Store the current theme in our private data
        $MyInvocation.MyCommand.Module.PrivateData["Theme"] = $Theme
        # Also store the current theme on the Host.PrivateData which survives module reload
        # Trace-Message -Verbose "Setting Host.PrivateData.Theme $($Theme.Name)"
        if ($Host.PrivateData.Theme -and $Host.PrivateData.Theme -is [Theme]) {
            # Instead of overwriting the theme, just update the modules we're importing:
            $Host.PrivateData.Theme = $Theme
        } else {
            $Host.PrivateData | Add-Member -NotePropertyName Theme -NotePropertyValue $Theme -Force -ErrorAction SilentlyContinue
        }

        # Trace-Message -Verbose "Import Module Configuration to change Theme"
        # Also export it to the configuration which survives PowerShell sessions (and affects new sessions)
        $Configuration = Import-Configuration
        $Configuration.Theme = $Theme.Name
        # Trace-Message -Verbose "Export Module Configuration with changed theme"
        $Configuration | Export-Configuration

        if ($Force -or $PSBoundParameters.ContainsKey("IncludeModule")) {
            # Trace-Message -Verbose "Either Force ($Force) or IncludeModule $($IncludeModule -join ',')"
            foreach ($module in $Theme.Modules) {
                # Trace-Message -Verbose "Import-Module $module -Scope Global"
                Write-Debug "Importing $module because it was Included or Forced by hand"
                if (!(Get-Module $Module)) { # Only try importing if the module isn't loaded already
                    Import-Module $module -ErrorAction SilentlyContinue -Scope Global -Verbose:$false
                }
            }
            $SupportedModules = @(Get-Module).Where{ $_.PrivateData -is [Collections.IDictionary] -and $_.PrivateData.ContainsKey("EzTheme") }
        }

        # Trace-Message -Verbose "Modules imported. Must theme modules"
        foreach ($module in $Theme.Modules) {
            # No point themeing modules that aren't imported?
            if ($module -notin @($SupportedModules.Name)) {
                # Trace-Message -Verbose "Skipping $module because it's not loaded"
                continue
            }

            try {
                # Trace-Message -Verbose "Themeing $module"
                $TheModule = $SupportedModules.Where({$module -eq $_.Name}, "First", 1)[0]
                Write-Verbose "Set the $Name theme for $TheModule $($Theme.Item($module) | Out-String)"
                $Theme.Item($module) | & "$($TheModule.Name)\$($TheModule.PrivateData["EzTheme"]["Set"])"
                # Trace-Message -Verbose "Themed $module"
            } catch {
                Write-Warning "Unable to set theme for $($module)\$($TheModule.PrivateData["EzTheme"]["Set"])"
            }
        }
        # Trace-Message -Verbose "END Import-Theme" -KillTimer
    }
}
#EndRegion '.\Public\Import-Theme.ps1' 102
#Region '.\Public\Show-Theme.ps1' 0
function Show-Theme {
    <#
        .SYNOPSIS
            Import a theme and output the custom PSObjects
    #>

    [Alias("shth")]
    [OutputType([string])]
    [CmdletBinding(DefaultParameterSetName="CurrentTheme")]
    param(
        # The name of the theme to export the current settings
        [Alias("Theme","PSPath")]
        [Parameter(ValueFromPipelineByPropertyName, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [ArgumentCompleter({
            param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
            Get-Theme $wordToComplete*
        })]
        [string]$Name,

        # One or more modules to export the theme from (ignores registered modules)
        [Parameter()]
        [AllowEmptyCollection()]
        [Alias("Module")]
        [string[]]$IncludeModule = "*"
    )
    process {
        # Without a theme name, show the current configuration
        $Themes = if (!$Name) {
            @($MyInvocation.MyCommand.Module.PrivateData.Theme)
        } else {
            Get-Theme $Name -Module $IncludeModule
        }

        foreach ($Theme in $Themes) {
            foreach ($module in $IncludeModule) {
                foreach ($ModuleTheme in $Theme.Modules.Where({ ($_ -like $Module) -or $_ -eq "Theme.$Module" -or $_ -eq "$Module.Theme" })) {
                    Write-Host "$([char]27)[0m$ModuleTheme $($Theme.Name) theme:"
                    foreach ($ThemeObject in $Theme.Item($ModuleTheme)) {
                        $ThemeObject | Out-Default
                    }
                }
            }
        }
    }
}
#EndRegion '.\Public\Show-Theme.ps1' 46
#Region '.\postfix.ps1' 0
function InitializeTheme {
    [CmdletBinding()]
    param()
    if (($script:Configuration = Import-Configuration).Theme) {
        Import-Theme $Configuration.Theme
    }
}

# InitializeTheme
#EndRegion '.\postfix.ps1' 10