Creating a Multichoice CLI Selector in PowerShell
Tags: tutorial, powershell, cli, programming
Published: 2023-07-10 18:45:15


In this article, we will explore how to create a command-line interface (CLI) multichoice selector in PowerShell. This selector allows users to navigate through a list of options and make multiple selections using the keyboard. We will break down the code provided and explain the relevant parts to understand how it works.

This is how it will look:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/proxuq0pq5buvh9bk1y0.png)


### Setting Up Terminal Manipulation Functions

Let's start by setting up the terminal manipulation functions that will allow us to control the appearance of the terminal window.

```powershell
# Function to clear the terminal
function Clear-Terminal {
  $ESC = [char]27
  $hideCursor = "${ESC}[?25l"
  Write-Host -NoNewline $hideCursor # Hide terminal cursor

  [Console]::SetCursorPosition(0, 0)
  [Console]::Clear()
}

# Function to close the terminal
Function Close-Terminal {
  $ESC = [char]27
  $showCursor = "${ESC}[?25h"

  Write-Host -NoNewline $showCursor
}
```

Here, we have two functions: `Clear-Terminal` and `Close-Terminal`. The `Clear-Terminal` function clears the terminal screen and hides the cursor, while the `Close-Terminal` function shows the cursor again when we want to exit the script. These functions use escape sequences to hide and show the cursor, respectively. They provide a cleaner and more interactive user experience.

### Function to Confirm the Selection and Exit

Next, we have the function responsible for confirming the selected options and exiting the script.

```powershell
# Function to confirm the selection and exit
function Confirm-Selection {
  $selectedOptions.sort()
  $selectedOptionNames = $selectedOptions | ForEach-Object { $listOptions[$_] }

  Write-Host "`nSelected Options: $($selectedOptionNames -join ", ")`n"

  Close-Terminal

  exit
}
```

In this function, the selected options are sorted, and their corresponding names are retrieved from the `$listOptions` array. These selected option names are then displayed using `Write-Host`. Finally, the `Close-Terminal` function is called to show the cursor, and the script exits.

### Function to Display the List with Selected Options

Now, let's take a look at the function responsible for rendering the list of options on the screen.

```powershell
# Function to display the list with the selected options
function Display-List {
  Clear-Terminal

  Write-Host "Select option(s) (press spacebar to toggle selection):`n"

  $listOptions | ForEach-Object -Begin { $index = 0 } -Process {
    $isSelected = $selectedOptions.Contains($index)
    $prefix = if ($index -eq $selectedOption.Value) { [char]0x276F } else { " " }
    $checkbox = if ($isSelected) { [char]0x25C9 } else { [char]0x25CB }
    $formattedOption = "$prefix $checkbox $_"

    Write-Host $formattedOption
    $index++
  }
}
```

This function first calls `Clear-Terminal` to clear the terminal screen and hide the cursor. Then, it displays the prompt message using `Write-Host`. Next, it iterates over the `$listOptions` array using `ForEach-Object`.

Inside the loop, it checks whether the current option is selected by using `$selectedOptions.Contains($index)`. Based on the selection status, it assigns different values to the `$checkbox` variable: "[char]0x25C9" (filled circle) if selected or "[char]0x25CB" (empty circle) if not selected. It also assigns a prefix to highlight the currently selected option using "[char]0x276F" (right-pointing arrow) or a space.

After formatting the option string, it is displayed on the screen using `Write-Host`. Finally, the `$index` variable is incremented to keep track of the option's index.

### Function to Handle Key Press Events

Now, let's explore the function responsible for handling different keypress events.

```powershell
# Function to handle key press events
function Handle-Key-Press([System.ConsoleKeyInfo]$keyInfo) {
  $key = $keyInfo.Key

  # Perform actions based on the pressed key
  switch ($key) {
    "DownArrow" {
      $selectedOption.Value = (($selectedOption.Value + 1) % $listOptions.Length)
      Display-List($selectedOption.Value, $selectedOptions)
    }

    "UpArrow" {
      $selectedOption.Value = (($selectedOption.Value - 1 + $listOptions.Length) % $listOptions.Length)
      Display-List($selectedOption.Value, $selectedOptions)
    }

    "Spacebar" {
      $currentOption = $selectedOption.Value
      $isSelected = $selectedOptions.Contains($currentOption)

      Write-Host "isSelected: $isSelected"

      if ($isSelected) {
        $optionIndex = $selectedOptions.IndexOf($currentOption)
        $selectedOptions.RemoveAt($optionIndex)
      } else {
        $selectedOptions.Add($currentOption)
      }

      Display-List($selectedOption.Value, $selectedOptions)
    }

    "Enter" {
      Confirm-Selection
    }
  }
}
```

This function takes a `[System.ConsoleKeyInfo]` object, `$keyInfo`, as input, which contains information about the pressed key.

Inside the function, a switch statement is used to perform actions based on the pressed key. Here's what each case does:

- **DownArrow**: Moves the currently selected option one position down in the list. It updates the `$selectedOption.Value` by incrementing it by 1 modulo the length of the `$listOptions` array. Then, it calls `Display-List` to refresh the list on the screen with the new selected option.

- **UpArrow**: Moves the currently selected option one position up in the list. It updates the `$selectedOption.Value` by decrementing it by 1 and adding the length of the `$listOptions` array modulo the length of the `$listOptions` array. This calculation ensures that the index wraps around correctly. Then, it calls `Display-List` to refresh the list on the screen with the new selected option.

- **Spacebar**: Toggles the selection of the current option. It checks if the current option is already selected by verifying its presence in the `$selectedOptions` array. If it is selected, it removes it from the array; otherwise, it adds it. Then, it calls `Display-List` to refresh the list on the screen with the updated selection.

- **Enter**: Confirms the selection by calling the `Confirm-Selection` function.

These key press events provide the core functionality of the multichoice selector.

### Initializing Variables and Displaying the Initial List

Before we start the main script loop, we need to initialize some variables and display the initial list.

```powershell
# List options
$listOptions = @("Barcelona", "Munich", "Paris", "Turin")

# Selected options
$selectedOptions = New-Object System.Collections.ArrayList

# Current selected option
$selectedOption = [ref]0

# Display the initial list
Display-List($selectedOption.Value, $selectedOptions)
```

Here, we define the `$listOptions` array, which contains the available

options to choose from. We also create the `$selectedOptions` variable as an instance of the `System.Collections.ArrayList` class to store the indices of the selected options. The `$selectedOption` variable is initialized as a reference to the currently selected option, starting with the index 0.

After initializing the variables, the initial list is displayed on the screen by calling the `Display-List` function with the current selection information.

### Main Script Loop

Finally, we have the main script loop that continuously waits for keypress events and handles them accordingly.

```powershell
while ($true) {
  $keyInfo = [System.Console]::ReadKey($true)
  Handle-Key-Press($keyInfo)

  Start-Sleep -Milliseconds 50
}
```

The loop runs indefinitely (`while ($true)`) and uses `[System.Console]::ReadKey($true)` to read a keypress without displaying it on the screen. The obtained `[System.ConsoleKeyInfo]` object is then passed to the `Handle-Key-Press` function for further processing.

A short sleep delay of 50 milliseconds is added using `Start-Sleep` to prevent excessive CPU usage during the loop.

By combining all these functions and the main script loop, we create a CLI multichoice selector in PowerShell that allows users to navigate through options, select or deselect them, and confirm their selections.

Feel free to experiment with the code and enhance it to suit your specific requirements. Happy coding!

```powershell
# Function to clear the terminal
function Clear-Terminal {
  $ESC = [char]27
  $hideCursor = "${ESC}[?25l"
  Write-Host -NoNewline $hideCursor # Hide terminal cursor

  [Console]::SetCursorPosition(0, 0)
  [Console]::Clear()
}

# Function to close the terminal
Function Close-Terminal {
  $ESC = [char]27
  $showCursor = "${ESC}[?25h"

  Write-Host -NoNewline $showCursor
}

# Function to confirm the selection and exit
function Confirm-Selection {
  $selectedOptions.sort()
  $selectedOptionNames = $selectedOptions | ForEach-Object { $listOptions[$_] }

  Write-Host "`nSelected Options: $($selectedOptionNames -join ", ")`n"

  Close-Terminal

  exit
}

# Function to display the list with the selected options
function Display-List {
  Clear-Terminal

  Write-Host "Select option(s) (press spacebar to toggle selection):`n"

  $listOptions | ForEach-Object -Begin { $index = 0 } -Process {
    $isSelected = $selectedOptions.Contains($index)
    $prefix = if ($index -eq $selectedOption.Value) { [char]0x276F } else { " " }
    $checkbox = if ($isSelected) { [char]0x25C9 } else { [char]0x25CB }
    $formattedOption = "$prefix $checkbox $_"

    Write-Host $formattedOption
    $index++
  }
}

# Function to handle key press events
function Handle-Key-Press([System.ConsoleKeyInfo]$keyInfo) {
  $key = $keyInfo.Key

  # Perform actions based on the pressed key
  switch ($key) {
    "DownArrow" {
      $selectedOption.Value = (($selectedOption.Value + 1) % $listOptions.Length)
      Display-List($selectedOption.Value, $selectedOptions)
    }

    "UpArrow" {
      $selectedOption.Value = (($selectedOption.Value - 1 + $listOptions.Length) % $listOptions.Length)
      Display-List($selectedOption.Value, $selectedOptions)
    }

    "Spacebar" {
      $currentOption = $selectedOption.Value
      $isSelected = $selectedOptions.Contains($currentOption)

      Write-Host "isSelected: $isSelected"

      if ($isSelected) {
        $optionIndex = $selectedOptions.IndexOf($currentOption)
        $selectedOptions.RemoveAt($optionIndex)
      } else {
        $selectedOptions.Add($currentOption)
      }

      Display-List($selectedOption.Value, $selectedOptions)
    }

    "Enter" {
      Confirm-Selection
    }
  }
}

# List options
$listOptions = @("Barcelona", "Munich", "Paris", "Turin")

# Selected options
$selectedOptions = New-Object System.Collections.ArrayList

# Current selected option
$selectedOption = [ref]0

# Display the initial list
Display-List($selectedOption.Value, $selectedOptions)

# Main script
while ($true) {
  $keyInfo = [System.Console]::ReadKey($true)
  Handle-Key-Press($keyInfo)

  Start-Sleep -Milliseconds 50
}
```

If you have any questions or need further assistance, please let me know in the comments section below or message me [Twitter](https://twitter.com/aagamezl) or [LinkedIn](https://www.linkedin.com/in/alvarojoseagamezlicha). Happy coding!.