Private/Install/Read-PwshProfileFeatureTree.ps1
|
function Read-PwshProfileFeatureTree { <# .SYNOPSIS Prompts the user with a grouped, all-checked-by-default feature tree and returns the feature tokens left enabled. .DESCRIPTION Drives the Install-PwshProfile wizard's feature step. The features are grouped under three startup sections — Shell, Prompt, Tools — so the user can toggle an individual feature or a whole section at once. Shell completions live under Tools (they are operations on the tools). Every feature starts CHECKED; unchecking one opts it out (the wizard maps the complement to -Skip / -SkipSection). oh-my-posh is deliberately absent from the tree: it has no skip switch and always runs, so listing it would be a checkbox that does nothing. Above the prompt a grey legend (Write-PwshProfilePromptHelp) describes each feature in one line — including the note that oh-my-posh is always enabled — since the Spectre tree can't carry per-item descriptions. The grouped multi-selection is built directly on the Spectre.Console MultiSelectionPrompt API rather than Read-SpectreMultiSelectionGrouped, because that wrapper cannot pre-check items (and "all on by default" is the whole point here). The prompt's string values are the human-readable feature labels; this function maps the returned labels back to their tokens. If the Spectre prompt types are unavailable (non-interactive host), it degrades by returning every feature token — i.e. everything enabled — matching the module's non-interactive fallback elsewhere. .PARAMETER Enabled A hashtable mapping each feature token (PSReadLine, TerminalIcons, PoshGit, Zoxide, Fzf, Fnm, Xh, Completions) to a boolean for its initial checked state. Missing/true tokens start checked. On the first wizard pass every token is enabled; when the step is re-edited from the review hub, the caller passes the current state so prior choices are preserved. .PARAMETER Color The accent color for the prompt's highlight style and the **tool name** spans in the legend, as a string — a hex value like '#c9aaff' or a Spectre color name like 'Silver'. Converted to a Spectre.Console.Color via Get-SpectreColorValue for the highlight style. .PARAMETER CodeColor The color for `code literal` spans (commands, cmdlets) in the legend, as a hex value or Spectre color name. Defaults to a soft cyan (#5fd7ff). .EXAMPLE Read-PwshProfileFeatureTree -Enabled @{ PSReadLine = $true; Fnm = $false } -Color '#c9aaff' Shows the tree with everything checked except Fast Node Manager, and returns the tokens the user leaves checked. #> [CmdletBinding()] [OutputType([string[]])] param( [Parameter()] [hashtable]$Enabled = @{}, [Parameter()] [string]$Color = '', [Parameter()] [string]$CodeColor = '#5fd7ff' ) # Section -> ordered features (display label <-> Initialize-PwshProfile token). 'Shell completions' # sits under Tools (completions are operations on the tools) and is skipped via -Skip Completions, # like the other tools — at runtime it registers as the final sub-step of the Tools section. $sections = [ordered]@{ Shell = @( [pscustomobject]@{ Label = 'PSReadLine config'; Token = 'PSReadLine' } ) Prompt = @( [pscustomobject]@{ Label = 'Terminal-Icons'; Token = 'TerminalIcons' } [pscustomobject]@{ Label = 'posh-git'; Token = 'PoshGit' } ) Tools = @( [pscustomobject]@{ Label = 'Zoxide (smart cd)'; Token = 'Zoxide' } [pscustomobject]@{ Label = 'fzf (fuzzy finder)'; Token = 'Fzf' } [pscustomobject]@{ Label = 'Fast Node Manager (fnm)'; Token = 'Fnm' } [pscustomobject]@{ Label = 'xh (HTTP client)'; Token = 'Xh' } [pscustomobject]@{ Label = 'Shell completions'; Token = 'Completions' } ) } $allFeatures = foreach ($key in $sections.Keys) { $sections[$key] } $allTokens = @($allFeatures | ForEach-Object { $_.Token }) $labelToToken = @{} foreach ($f in $allFeatures) { $labelToToken[$f.Label] = $f.Token } # A token is checked unless the caller explicitly disabled it. $isEnabled = { param($token) -not ($Enabled.ContainsKey($token) -and -not $Enabled[$token]) } # Non-interactive / Spectre unavailable: everything enabled. if (-not ('Spectre.Console.MultiSelectionPrompt`1' -as [type])) { return $allTokens } # A per-feature legend above the tree, so each checkbox has context (the Spectre tree itself can't # carry per-item descriptions). oh-my-posh isn't a checkbox — it always runs — so it's noted here. $accent = if ($Color) { $Color } else { '#c9aaff' } Write-PwshProfilePromptHelp @( '**oh-my-posh** is always enabled — it draws the prompt and has no checkbox.' '**PSReadLine** config — nicer command-line editing: history search, syntax colors, prediction.' '**Terminal-Icons** — file-type icons in directory listings (`ls` / `Get-ChildItem`).' '**posh-git** — git branch and status shown right in the prompt.' '**Zoxide** (smart `cd`) — a cd that learns your most-used dirs so you can jump by partial name.' '**fzf** (fuzzy finder) — a fast command-line fuzzy picker; when on PATH, zoxide uses it for its interactive `cdi`/`zi` jump.' '**Fast Node Manager** (`fnm`) — install and switch between Node.js versions per project.' '**xh** (HTTP client) — a fast, friendly `curl`/HTTPie-style tool for making HTTP requests.' '**Shell completions** — Tab completion for `winget`, `tailscale`, `docker`, and `op`.' ) -Accent $accent -Code $CodeColor $prompt = [Spectre.Console.MultiSelectionPrompt[string]]::new() $prompt.Title = 'Select the features to enable (Space toggles a feature or a whole section; Enter submits)' $prompt.PageSize = 12 $prompt.WrapAround = $true $prompt.Required = $false $prompt.HighlightStyle = [Spectre.Console.Style]::new((Get-SpectreColorValue $accent)) foreach ($key in $sections.Keys) { $labels = @($sections[$key] | ForEach-Object { $_.Label }) $prompt = [Spectre.Console.MultiSelectionPromptExtensions]::AddChoiceGroup($prompt, $key, [string[]]$labels) } # Pre-check every currently-enabled feature so the tree opens with the user's state (all on by # default on the first pass). A fully-enabled section also gets its header checked — in Leaf mode # the parent's box is derived from children only during interaction, not at the initial render, so # without this the section shows unchecked while its features show checked. A partially-enabled # section is left unselected so it correctly shows as partial. foreach ($key in $sections.Keys) { $children = @($sections[$key]) $enabledChildren = @($children | Where-Object { & $isEnabled $_.Token }) if ($enabledChildren.Count -eq $children.Count) { $prompt = [Spectre.Console.MultiSelectionPromptExtensions]::Select($prompt, $key) } foreach ($c in $enabledChildren) { $prompt = [Spectre.Console.MultiSelectionPromptExtensions]::Select($prompt, $c.Label) } } $selectedLabels = @($prompt.Show([Spectre.Console.AnsiConsole]::Console)) $selectedTokens = @($selectedLabels | ForEach-Object { $labelToToken[$_] } | Where-Object { $_ }) return $selectedTokens } |