Private/Install/Read-PwshProfileFeatureTree.ps1
|
function Read-PwshProfileFeatureTree { <# .SYNOPSIS Prompts the user with a grouped, opt-in feature tree and returns the tokens the user checked. .DESCRIPTION Drives the Install-PwshProfile wizard's feature step. The features come from Get-PwshProfileToolCatalog and are grouped under two sections — Core and WinGet — so the user can toggle an individual feature or a whole section at once. The shell completions live under Core. This function is purely seed-driven: a feature starts CHECKED only when -Enabled marks it (so an empty map opens with everything unchecked), and the wizard maps the checked set to -Enable. The caller decides the seed — a re-run passes the prior -Enable set, a clean first run passes the Core default-on set (Get-PwshProfileToolCatalog -DefaultEnabled), so Core opens checked and WinGet unchecked. oh-my-posh is deliberately absent from the tree: it 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. Tools in -New are tagged "(new)" with a legend note so additions since the prior setup stand out. The grouped multi-selection is built directly on the Spectre.Console MultiSelectionPrompt API rather than Read-SpectreMultiSelectionGrouped, because that wrapper cannot pre-check items. The prompt's string values are the (possibly "(new)"-tagged) 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 just the tokens marked enabled in -Enabled (whatever the caller seeded), matching the module's non-interactive fallback elsewhere. .PARAMETER Enabled A hashtable mapping each feature token (from Get-PwshProfileToolCatalog) to a boolean for its initial checked state. Selection is seed-driven: a token is checked ONLY when its value is $true, so an empty map opens with everything unchecked. The caller supplies the seed — a re-run passes the prior -Enable set, a clean first run passes the Core default-on set (Core checked, WinGet unchecked). .PARAMETER New Tokens that are newly available since the prior setup (current catalog minus the recorded snapshot). Their labels are tagged "(new)" and a legend note calls them out; they still start unchecked (per the opt-in rule) so the user consciously adopts them. .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[]]$New = @(), [Parameter()] [string]$Color = '', [Parameter()] [string]$CodeColor = '#5fd7ff' ) # Section -> ordered features (label <-> -Enable token) from the single-source catalog. Tag tokens # in -New with a "(new)" suffix so additions since the prior setup stand out; the catalog returns # fresh objects each call, so mutating the labels here is safe. $sections = Get-PwshProfileToolCatalog $newSet = @($New) foreach ($key in $sections.Keys) { foreach ($f in $sections[$key]) { if ($newSet -contains $f.Token) { $f.Label = "$($f.Label) (new)" } } } $allFeatures = foreach ($key in $sections.Keys) { $sections[$key] } $allTokens = @($allFeatures | ForEach-Object { $_.Token }) $labelToToken = @{} foreach ($f in $allFeatures) { $labelToToken[$f.Label] = $f.Token } # Opt-in: a token is checked only when the caller marked it enabled. $isEnabled = { param($token) [bool]($Enabled.ContainsKey($token) -and $Enabled[$token]) } # Non-interactive / Spectre unavailable: return only the tokens already marked enabled (nothing on # a first run), matching the opt-in model rather than turning everything on. if (-not ('Spectre.Console.MultiSelectionPrompt`1' -as [type])) { return @($allTokens | Where-Object { & $isEnabled $_ }) } # 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' } $legend = @( '**oh-my-posh** is always enabled — it draws the prompt and has no checkbox.' '**Core** features are checked by default; the **WinGet** group is unchecked — checking one installs that tool via `winget`.' '**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 (full UI style; via PSFzf adds `Ctrl+T` file picker with a `bat` preview, `Ctrl+R` fuzzy history, and `Ctrl+G` git pickers); when on PATH, zoxide uses it for its interactive `cdi`/`zi` jump.' '**fnm** (Fast Node Manager) — install and switch between Node.js versions per project.' '**xh** (HTTP client) — a fast, friendly `curl`/HTTPie-style tool for making HTTP requests.' '**jq** (JSON processor) — a lightweight command-line JSON query and transformation tool.' '**bat** (cat replacement) — a `cat` with syntax highlighting and git integration; its theme blends with the prompt. You can replace the built-in `cat` with it.' '**fd** (file finder) — a fast, friendly `find` alternative that respects `.gitignore`; its colors blend with the prompt and, with fzf, drive fzf''s file search. Standalone — it does not replace `Get-ChildItem`.' '**less** (pager) — a full-featured pager (color, search, backward scroll) that replaces the limited `more.com`; it is what lets `bat` page with color. You can route `help`/`more` and color CLIs through it.' '**Shell completions** — Tab completion for `winget`, `tailscale`, `docker`, and `op`.' ) if ($newSet.Count) { $legend += 'Items tagged **(new)** were added to the module since your last setup — they start unchecked.' } Write-PwshProfilePromptHelp $legend -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 seeded state (Core on, # WinGet off on a clean first run). 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 } |