EzTheme.psm1
using module @{ModuleName = "Configuration"; ModuleVersion = "1.5.1"} 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.5.1"} #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 |