A few months ago, I wrote about my PSClock module. This is a set of PowerShell commands for creating a digital clock using a transparent WPF form. You can install the module from the PowerShell Gallery. The repository can be found at https://github.com/jdhitsolutions/PSClock. Because my Windows wallpaper changes throughout the day, sometimes I need to change the clock color to make it easier to read. The challenge is that the color options come from [System.Drawing.Color]. I can use values like PapayaWhip, Cornsilk, and Moccasin. But what do those colors look like? Let's have some fun and figure it out.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
The first step, which is more of a safety net than anything, is to make sure I can use [System.Drawing.Color] in my PowerShell session. I'll use Add-Type to load the assembly.
Try {
Add-Type -AssemblyName system.drawing -ErrorAction Stop
}
Catch {
Throw "These functions require the [System.Drawing.Color] .NET Class"
}
As far as I know, PowerShell has no way of directly rendering a color value from this class. However, PowerShell can, in fact, render color using ANSI escape sequences, but not in the PowerShell ISE. We'll head in that direction.
Getting Color Values
I can get a color directly from the class using syntax like this.
The properties R, G, and B are the Red, Green, and Blue values. I'll need those in a moment—first, a few quick words about this class.
How did I know the color name? I asked PowerShell using PSReadline.
After the :: operator, I press Ctrl+Space and answer y when prompted.
The "gotcha" for those familiar with working with .NET classes is that this is NOT an enumeration. You cannot use code like this:
[enum]::GetNames([System.ConsoleColor])
But, I can use the GetProperties() method, select the Name property and filter out properties that don't match color names.
[system.drawing.color].GetProperties().name | Where-Object { $_ -notmatch "^\bIs|Name|[RGBA]\b" }
Once I know the name, I can get the value I did above or use the FromName() method.
Converting to ANSI
As I mentioned earlier, if I have RBG values, I can construct a 256-color ANSI escape sequence. The easiest way is to use the FromRGB() method on $PSStyle.Foreground in PowerShell 7.2.
$psstyle.Foreground.fromrgb(127,255,0)
Or you can use the actual ANSI sequence.
"$([char]27)[38;2;{0};{1};{2}m" -f 127,255,0
If you try this using the Chartreuse RBG values, you won't see anything other than maybe part of your prompt changing color. That's because this is only the opening part of a sequence. To use it, build a string like this:
#PS 7.2
$ansi = $PSStyle.Foreground.FromRgb(127,255,0)
"$($ansi)Chartreuse$($psstyle.Reset)"
#Other
$ansi = "$([char]27)[38;2;{0};{1};{2}m" -f 127,255,0
"$($ansi)Chartreuse$([char]27)[0m"
Toolmaking
At this point, I should have all the pieces I need. I can get the RGB values of the [System.Drawing.Color. I can use those values to create the ANSI sequence. Finally, I can build a sample string.
$name = "Turquoise"
$color = [System.Drawing.Color]::FromName($Name)
$ansi = $PSStyle.Foreground.FromRgb($color.R,$color.G,$color.B)
"$($ansi)$Name$($psstyle.Reset)"
You can almost see PowerShell functions writing themselves. I could have built a monolithic function to do everything, but I decided to break this down to a more granular level. Having separate functions makes them easy to re-use. It also makes them easier to Pester test. Especially because I'm invoking several .NET methods that you can't properly mock. But, I can mock a function like this.
function Get-RGB {
[cmdletbinding()]
[OutputType("RGB")]
Param(
[Parameter(Mandatory, HelpMessage = "Enter the name of a system color like Tomato")]
[ValidateNotNullOrEmpty()]
[string]$Name
)
Try {
$Color = [System.Drawing.Color]::FromName($Name)
[PSCustomObject]@{
PSTypeName = "RGB"
Name = $Name
Red = $color.R
Green = $color.G
Blue = $color.B
}
}
Catch {
Throw $_
}
}
The function writes a simple object to the pipeline. I can use that output to create the ANSI sequence.
function Convert-RGBtoAnsi {
#This will write an opening ANSI escape sequence to the pipeline
[cmdletbinding()]
[OutputType("String")]
Param(
[parameter(Position = 0, ValueFromPipelineByPropertyName)]
[int]$Red,
[parameter(Position = 1, ValueFromPipelineByPropertyName)]
[int]$Green,
[parameter(Position = 2, ValueFromPipelineByPropertyName)]
[int]$Blue
)
Process {
<#
For legacy powershell session you could create a string like this:
"$([char]27)[38;2;{0};{1};{2}m" -f $red,$green,$blue
#>
$psstyle.Foreground.FromRgb($Red, $Green, $Blue)
}
}
You'll notice that the parameters are defined with the same names as the RGB object and take pipeline input. That's because I knew I would want to run a command like this.
$name ="DeepSkyBlue"
$ansi = Get-RGB $name | Convert-RGBtoAnsi
"$($ansi)$Name$($psstyle.Reset)"
The last command makes it even easier to run these commands.
Function Get-DrawingColor {
[cmdletbinding()]
[alias("gdc")]
[OutputType("PSColorSample")]
Param(
[Parameter(Position = 0, HelpMessage = "Specify a color by name. Wildcards are allowed.")]
[ValidateNotNullOrEmpty()]
[string[]]$Name
)
Write-Verbose "Starting $($MyInvocation.MyCommand)"
if ($PSBoundParameters.ContainsKey("Name")) {
if ($Name[0] -match "\*") {
Write-Verbose "Finding drawing color names that match $name"
$colors = [system.drawing.color].GetProperties().name | Where-Object { $_ -like $name[0] }
}
else {
$colors = @()
foreach ($n in $name) {
if ($n -as [system.drawing.color]) {
$colors += $n
}
else {
Write-Warning "The name $n does not appear to be a valid System.Drawing.Color value. Skipping this name."
}
Write-Verbose "Using parameter values: $($colors -join ',')"
} #foreach name
} #else
} #if PSBoundParameters contains Name
else {
Write-Verbose "Geting all drawing color names"
$colors = [system.drawing.color].GetProperties().name | Where-Object { $_ -notmatch "^\bIs|Name|[RGBA]\b" }
}
Write-Verbose "Processing $($colors.count) colors"
if ($colors.count -gt 0) {
foreach ($c in $colors) {
Write-Verbose "...$c"
$ansi = Get-RGB $c -OutVariable rgb | Convert-RGBtoAnsi
#display an ANSI formatted sample string
$sample = "$ansi$c$($psstyle.reset)"
#write a custom object to the pipeline
[PSCustomObject]@{
PSTypeName = "PSColorSample"
Name = $c
RGB = $rgb
ANSIString = $ansi.replace("`e", "``e")
ANSI = $ansi
Sample = $sample
}
}
} #if colors.count > 0
else {
Write-Warning "No valid colors found."
}
Write-Verbose "Ending $($MyInvocation.MyCommand)"
}
I can specify a color by name.
I don't know all the ways I might use this output yet, so I'm creating a rich object. There is a value for the ANSI property, but it doesn't display because it is the actual ANSI sequence. That's why I include an ANSIString property that shows the value as a string I could use in a script.
The default behavior is to display everything.
Not every color displays using ANSI, and your background color may also affect the display, but this is close enough for my work. By the way, the function accepts wildcards.
This screenshot is from VS Code.
Custom Formatting
You probably noticed that I often used Format-Table to get better-looking output. In fact, I might like an output that only shows me the sample.
But let's make this easy. I created a format ps1xml file for the custom object from Get-DrawingColor. If you go back to the code, you'll see that I am assigning a type name. This allows me to create the file.
<!--
Format type data generated 02/08/2022 17:32:56 by PROSPERO\Jeff
This file was created using the New-PSFormatXML command that is part
of the PSScriptTools module.
https://github.com/jdhitsolutions/PSScriptTools
-->
<Configuration>
<ViewDefinitions>
<View>
<!--Created 02/08/2022 17:32:56 by PROSPERO\Jeff-->
<Name>default</Name>
<ViewSelectedBy>
<TypeName>PSColorSample</TypeName>
</ViewSelectedBy>
<WideControl>
<!--Delete the AutoSize node if you want to use PowerShell defaults.
<AutoSize />-->
<WideEntries>
<WideEntry>
<WideItem>
<PropertyName>Sample</PropertyName>
</WideItem>
</WideEntry>
</WideEntries>
</WideControl>
</View>
<View>
<!--Created 02/08/2022 17:35:40 by PROSPERO\Jeff-->
<Name>default</Name>
<ViewSelectedBy>
<TypeName>PSColorSample</TypeName>
</ViewSelectedBy>
<TableControl>
<!--Delete the AutoSize node if you want to use the defined widths.-->
<AutoSize />
<TableHeaders>
<TableColumnHeader>
<Label>Name</Label>
<Width>14</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>RGB</Label>
<Width>31</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>ANSIString</Label>
<Width>22</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Sample</Label>
<Width>37</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<!--
By default the entries use property names, but you can replace them with scriptblocks.
<ScriptBlock>$_.foo /1mb -as [int]</ScriptBlock>
-->
<TableColumnItem>
<PropertyName>Name</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>RGB</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>ANSIString</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Sample</PropertyName>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
</ViewDefinitions>
</Configuration>
In the script file that contains all of the functions, I add this line to load the formatting file.
Update-FormatData $PSScriptRoot\pscolorsample.format.ps1xml
I made the wide layout the default.
I also created a custom table view that omits the ANSI property since there's nothing to see.
Summary
I'll admit you may not have a compelling need for these functions. But I hope you followed my development process. Think about how your functions might work together. Write rich objects to the pipeline and use formatting files to provide default output.
Have a great weekend.
3 thoughts on “Friday Fun – Painting a Pretty Picture with PowerShell”
Comments are closed.