Home > Enterprise >  Is there any way to colour text previously written using "Write-Host" in powershell?
Is there any way to colour text previously written using "Write-Host" in powershell?

Time:12-17

I want to create the "Select-Multiple" function.

The function takes some parameters, but the most important one is the list of options. Let's say @("First Option", "Second Option")

Then the function will display something like:

a All
b First Option
c Second Option
d Exit
Choose your option: > ...

The "Choose your option: > ..." text, will be repeated as long as:

  1. User choose "All" or "Exit" option
  2. User will choose all possible options (other than "All" and "Exit")

At the end the function returns the List of options chosen by the user.

Simple. But... I'd like to highlight the options already chosen by the user. So if the user chose "b", then "b First Option" gets green colour.

Is it possible to do something like that, without using Clear-Host, as I don't want to clear previous steps?

I attach you my "Select-Multiple" function in powershell, sorry if that's ugly written, but I don't use powershell that often.

function Select-Multiple {
    Param(
        [Parameter(Mandatory=$false)]
        [string] $title,
        [Parameter(Mandatory=$false)]
        [string] $description,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        $options,
        [Parameter(Mandatory=$true)]
        [string] $question
    )   
    if ($title) {
        Write-Host -ForegroundColor Yellow $title
        Write-Host -ForegroundColor Yellow ("-"*$title.Length)
    }

    if ($description) {
        Write-Host $description
        Write-Host
    }

    $chosen = @()
    $values = @()
    $offset = 0

    $all = "All"
    $values  = @($all)
    Write-Host -ForegroundColor Yellow "$([char]($offset 97)) " -NoNewline
    Write-Host $all
    $offset  
    
    $options.GetEnumerator() | ForEach-Object {
        Write-Host -ForegroundColor Yellow "$([char]($offset 97)) " -NoNewline
        $values  = @($_)
        Write-Host $_
        $offset   
    }

    $exit = "Exit"
    $values  = @($exit)
    Write-Host -ForegroundColor Yellow "$([char]($offset 97)) " -NoNewline
    Write-Host $exit

    $answer = -1
    while($chosen.Count -ne $options.Count) {
        Write-Host "$question " -NoNewline
        $selection = (Read-Host).ToLowerInvariant()
        if (($selection.Length -ne 1) -or (([int][char]($selection)) -lt 97 -or ([int][char]($selection)) -gt (97 $offset))) {
            Write-Host -ForegroundColor Red "Illegal answer. " -NoNewline
        }
        else {
            $answer = ([int][char]($selection))-97
            $value = $($values)[$answer]
            if ($value -eq $exit) {
                return $chosen
            }

            if ($value -eq $all) {
                return $options
            }
            else {
                if ($chosen.Contains($value)) {
                    Write-Host -ForegroundColor Red "The value $value was already chosen."
                }
                else {
                    $chosen  = ($value)
                }
            }
        }
        if ($answer -eq -1) {
            Write-Host -ForegroundColor Red "Please answer one letter, from a to $([char]($offset 97))"
        }
        $answer = -1;
    }
    return $chosen
}

CodePudding user response:

Because of how the console window works, you can't just recolor an existing line. Once you've written something to the console, the only way you can modify it is by overwriting it. This is no different when applying colors.

To understand why this is the case, let's go over how text is colored in PowerShell. Let's use the following command as an example:

Write-Host "Test" -ForegroundColor Green

Here is a (simplified) step by step overview of what this command will do:

  1. Set the ForegroundColor property of the console to green
  2. Write "Test" to the console
  3. Set the ForegroundColor property of the console to whatever it was previously

This explains why we are unable to change the color of text that has already been written to the console. If you want to color a piece of text, you are required to set the console color before writing the text to the console.

However, there are a couple ways to create the same visual effect. In fact there are exactly two ways. One of them is clearing the console and re-writing everything which you mentioned you don't want to do, so let's talk about the other way first.


Overwriting a Single Line

Let me preface this by saying that this does not work very well with the PowerShell ISE. If you decide to use this, you will have to debug it by either using the normal PowerShell console, or an IDE that supports this. This is also the more complicated option, so if you don't want to deal with the complexity of it, the second option would be the way to go.

The console window allows you to retrieve and set the cursor position by using Console.CursorLeft and Console.CursorTop. You can also use Console.SetCursorPosition() if you need to set both of them at the same time. There is also $Host.UI.RawUI.CursorPosition, but it's long and has some strange side effects when paired with Read-Host, so I would not recommend using it. When you write output to the console, it will write the output to wherever the cursor happens to be. We can use this to our advantage by setting the cursor's position to the beginning of the line we want to change the color of, then overwriting the normal text with colored text or vice versa.

In order to do this, all we need to do is keep track of which option is on which line. This is pretty simple, especially if you have an array of options that is in the same order that you printed them to the console in.

Here is a simple script I made that does exactly this:

$options = $("Option 1", "Option 2", "Option 3")
$initialCursorTop = [Console]::CursorTop

# An array to keep track of which options are selected.
# All entries are initially set to $false.
$selectedOptionArr = New-Object bool[] $options.Length

for($i = 0; $i -lt $options.Length; $i  )
{
    Write-Host "$($i   1). $($options[$i])"
}
Write-Host # Add an extra line break to make it look pretty

while($true)
{
    Write-Host "Choose an option>" -NoNewline
    $input = Read-Host
    $number = $input -as [int]
    if($number -ne $null -and 
       $number -le $options.Length -and 
       $number -gt 0)
    {
        # Input is a valid number that corresponds to an option.

        $oldCursorTop = [Console]::CursorTop
        $oldCursorLeft = [Console]::CursorLeft

        # Set the cursor to the beginning of the line corresponding to the selected option.
        $index = $number - 1
        [Console]::SetCursorPosition(0, $index   $initialCursorTop)
        $choice = $options[$index]
        $isSelected = $selectedOptionArr[$index]

        $choiceText = "$($number). $($choice)"
        if($isSelected)
        {
            Write-Host $choiceText -NoNewline
        }
        else
        {
            Write-Host $choiceText -ForegroundColor Green -NoNewline
        }
        $selectedOptionArr[$index] = !$isSelected
        

        [Console]::SetCursorPosition($oldCursorLeft, $oldCursorTop)
    }

    # Subtract 1 from Y to compensate for the new line created when providing input.
    [Console]::SetCursorPosition(0, [Console]::CursorTop - 1)

    # Clear the input line.
    Write-Host (' ' * $Host.UI.RawUI.WindowSize.Width) -NoNewline
    [Console]::CursorLeft = 0
}

The main advantage of this approach is that it doesn't need to clear the entire console in order to update text. This means you can display whatever you want above it without worrying about it being cleared every time the user inputs something. Another advantage is that this performs a minimal number of operations in order to perform the task.

The main disadvantage is that this is relatively volatile. This requires you to use exact line numbers, so if something happens that offsets some of the lines (such as one option being multiple lines), it will more than likely cause some major issues.

However, these disadvantages can be overcome. Since you have access to $Host.UI.RawUI.WindowSize.Width which tells you how many characters you can put on a single line, we know that any string with a length greater than this will be wrapped onto multiple lines. Another option is just to keep track of which line the cursor starts on, then you can clear all text between the starting position and where the cursor currently is.


Clearing the Console

This approach is much simpler because you don't have to worry about what is on which line or where the cursor is at all. The idea is that you simply clear the entire console, then re-write everything with the changes you want to make. This is the nuclear approach, but it's also the most reliable.

Here is the same example as above using this approach instead:

$options = $("Option 1", "Option 2", "Option 3")

# An array to keep track of which options are selected.
# All entries are initially set to $false.
$selectedOptionArr = New-Object bool[] $options.Length

while($true)
{
    Clear-Host

    for($i = 0; $i -lt $options.Length; $i  )
    {
        if($selectedOptionArr[$i])
        {
            Write-Host "$($i   1). $($options[$i])" -ForegroundColor Green
        }
        else
        {
            Write-Host "$($i   1). $($options[$i])"
        }
    }
    Write-Host # Add an extra line break to make it look pretty
    Write-Host "Choose an option>" -NoNewline

    $input = Read-Host
    $number = $input -as [int]

    if($number -ne $null -and 
       $number -le $options.Length -and 
       $number -gt 0)
    {
        # Input is a valid number that corresponds to an option.

        $index = $number - 1
        $choice = $options[$index]
        $selectedOptionArr[$index] = !$selectedOptionArr[$index]
    }
}

The main advantage of this approach is that it's super simple and easy to understand.

The main disadvantage is that this clears the entire console every time the user inputs something. In most cases this isn't a huge problem, but it can be with large data sets.

CodePudding user response:

I agree with Trevor Winge, I it is most likely not possible to change earlier console outputs appearance, but it is for certain not worth your while. There are certain limitations of the console, and that is ok since that is what GUIs are for. I hope you feel encouraged by this situation to look into Windows.Froms or WPF. Make use of the controls! checkboxes would be interesting for your scenario. I know that is not exactly what you asked for, but it is garanteed a journey that is worth your time. When i started my first GUI with powershell, i was astonished how much i could accomplish with little afford. Stackoverflow is full of examples.

  • Related