Private/Install/Invoke-PwshProfileWizard.ps1
|
function Invoke-PwshProfileWizard { <# .SYNOPSIS Runs the interactive Install-PwshProfile setup wizard and returns the chosen settings (or $null if the user cancels). .DESCRIPTION Drives the PwshSpectreConsole prompts that collect the user's profile configuration and returns a settings hashtable (the keys of Get-PwshProfileDefault, plus a NerdFont key holding the chosen Nerd Font name(s) as an array, or $null when none were selected, plus the WingetScope / WingetProgressBar / WingetAnonymizePath / WingetDisableInstallNote keys carrying the chosen winget client settings). If the user cancels at the review screen, it returns $null and Install-PwshProfile writes nothing. Each step opens with a rounded header panel (Write-PwshProfileStepHeader) carrying the step title, a "N of M" progress counter, and a primary description; secondary prompts get inline hint lines (Write-PwshProfilePromptHelp). Both run their text through Format-PwshProfileHelpMarkup, so tool names and code literals are highlighted rather than flat grey — users unfamiliar with the underlying tools (zoxide and its jump command especially) aren't left guessing. Selection prompts (Read-SpectreSelection) clear themselves on submit, unlike the text prompts that leave their answer on screen, so each selection's chosen value is echoed afterward via Write-PwshProfilePromptAnswer (an accent check mark + the value) to keep a visible record. The wizard makes one forward pass through the steps, then lands on a review hub where any step can be re-edited before submitting, or the whole thing cancelled: 1. Nerd Fonts: optional, a single yes/no; on yes, ensures the NerdFonts module and installs the recommended Meslo + CascadiaCode pair; on no, nothing is installed. 2. Winget: a curated set of winget client settings (default install scope, progress-bar style, anonymize-displayed-paths, suppress-install-notes). It first shows the current values (pre-filled from the live settings.json via Get-WingetSettingDefault, flagging any that differ from the recommendation) and asks whether to change them — defaulting to No, via Read-PwshProfileSettingChange — only prompting per-setting on Yes. The values are applied to settings.json at install time by Install-PwshProfile via Set-WingetSetting (not baked into the bootstrap call). 3. Theme: pick a bundled oh-my-posh theme (screwcity / forestcity) or supply a custom theme path. The bundled choice seeds the banner color and step icon the later prompts are pre-filled with; a custom path seeds neutral color/icon (a neutral color, a generic icon) so you define those fresh. The banner text defaults to the machine name regardless of theme. Re-picking a theme later preserves any color/icon you already customized (only still-default fields are re-seeded). 4. Banner: shows the current banner config (shown/hidden plus text/color/alignment/font, flagging anything off the theme default) and asks whether to change it — defaulting to No, via Read-PwshProfileSettingChange. On Yes it asks a show/hide yes-no (no disables the banner via -Skip Banner and skips the theming sub-steps; yes prompts text, color, alignment, and bundled font). Clearing the banner text also hides the banner — an empty text renders nothing at startup, so it's treated like a declined banner (-Skip Banner, default text restored) rather than left as a shown-but-blank half-state. 5. Step icon: always asked (the icon marks every startup step, banner or not) — a curated shortcode menu with the current icon floated to the top, plus a "custom shortcode" escape. 6. Features: a grouped, all-checked-by-default tree (Read-PwshProfileFeatureTree) under the Shell / Prompt / Tools sections (shell completions sit under Tools); unchecking opts a feature (or a whole section) out, mapped to -Skip / -SkipSection. oh-my-posh is always on and not listed. If zoxide stays enabled, its jump command is prompted. Then a review panel summarizes the choices and offers Submit / Edit <step> / Cancel. Assumes the Spectre prompt cmdlets are available — Install-PwshProfile guards that and falls back to defaults when they are not. .PARAMETER Reconfiguring Indicates the target profile already contains a managed block, so the intro line can say it is updating rather than creating. Purely cosmetic. .EXAMPLE Invoke-PwshProfileWizard Walks the user through the prompts and returns the resulting settings hashtable (or $null if cancelled). #> [CmdletBinding()] param( [Parameter()] [switch]$Reconfiguring ) # Shared mutable state, passed by reference into every step so edits from the review hub stick. # Settings is the hashtable returned to the caller; Def is the baseline for the *current* theme # (drives pre-fills and the "still default?" preserve-edits check); Accent / Code are the # installer's own UI colors — fixed at the module's signature purple and a soft cyan, and # intentionally decoupled from the prompt theme being configured, so the wizard (panels, accents, # code-literal highlighting) looks the same whichever theme you pick. $def = Get-PwshProfileDefault $settings = $def.Clone() $settings.NerdFont = $null # winget client settings (applied to winget's settings.json at install time, like NerdFont — not # part of the Initialize-PwshProfile bootstrap, so Build-PwshProfileInitializeCall ignores them). # Seed from the live settings file: an explicitly-set value becomes the pre-fill, otherwise the # module default (Get-WingetSettingDefault). $wingetDef = Get-WingetSettingDefault $settings.WingetScope = $wingetDef.Scope $settings.WingetProgressBar = $wingetDef.ProgressBar $settings.WingetAnonymizePath = $wingetDef.AnonymizePath $settings.WingetDisableInstallNote = $wingetDef.DisableInstallNote $state = @{ Settings = $settings; Def = $def; Accent = '#c9aaff'; Code = '#5fd7ff' } # Escape a dynamic value for safe inclusion in Spectre markup (banner text, paths, …). $esc = { param($text) if ([string]::IsNullOrEmpty("$text")) { return '' } Get-SpectreEscapedTextSafe -Text "$text" } # --- Step: Theme ------------------------------------------------------------------------ $stepTheme = { param($s, $i, $total) Write-PwshProfileStepHeader -Title 'Theme' -Index $i -Total $total -Accent $s.Accent -Code $s.Code ` -Body '**oh-my-posh** draws your prompt — its colors, segments, and the layout of each line. Pick a bundled look or point to your own `.omp.json` file.' $themeChoices = @( @(Get-BundledThemeName) | ForEach-Object { $b = Get-BundledThemeBranding -Name $_ [pscustomobject]@{ Label = "$_ — $($b.DisplayName)"; Theme = $_; Custom = $false } } [pscustomobject]@{ Label = 'Custom path…'; Theme = $null; Custom = $true } ) # Float the default theme (screwcity) to the top so pressing Enter keeps it. $themeChoices = @($themeChoices | Where-Object { $_.Theme -eq 'screwcity' }) + @($themeChoices | Where-Object { $_.Theme -ne 'screwcity' }) $pickTheme = Read-SpectreSelection -Message 'Choose an oh-my-posh theme' -Color $s.Accent -Choices $themeChoices -ChoiceLabelProperty Label Write-PwshProfilePromptAnswer $pickTheme.Label -Accent $s.Accent # The branding (color/icon) the current fields were seeded from, so we only re-seed untouched # ones. Banner text is theme-independent ($env:COMPUTERNAME default), so it's not re-seeded. $neutral = @{ BannerColor = 'Silver'; StepIcon = ':gear:' } $prevBranding = if ($s.Settings.CustomTheme) { $neutral } else { Get-BundledThemeBranding -Name $s.Settings.Theme } if ($pickTheme.Custom) { do { Write-PwshProfilePromptHelp 'Enter the full path to an **oh-my-posh** theme file (a `.omp.json`) on disk.' -Accent $s.Accent -Code $s.Code $customPath = Read-SpectreText -Message 'Path to your custom oh-my-posh theme (.omp.json)' $pathOk = $customPath -and (Test-Path -Path $customPath) if (-not $pathOk) { Write-Warning "Theme path '$customPath' was not found; please try again." } } until ($pathOk) # A custom theme has no bundled identity, so its color/icon baseline is NEUTRAL. The banner # text keeps the uniform $env:COMPUTERNAME default. Theme stays 'screwcity' but is never # emitted, since -CustomTheme takes precedence in the generated call. $newDef = Get-PwshProfileDefault $newDef.BannerColor = 'Silver'; $newDef.StepIcon = ':gear:' $newBranding = $neutral $s.Settings.Theme = 'screwcity' $s.Settings.CustomTheme = $customPath } else { $newDef = Get-PwshProfileDefault -Theme $pickTheme.Theme $newBranding = Get-BundledThemeBranding -Name $pickTheme.Theme $s.Settings.Theme = $pickTheme.Theme $s.Settings.CustomTheme = '' } # Re-seed only the color/icon fields the user hasn't customized away from the old theme's # values (banner text is theme-independent, so it's never re-seeded here). foreach ($k in 'BannerColor', 'StepIcon') { if ($s.Settings[$k] -eq $prevBranding[$k]) { $s.Settings[$k] = $newBranding[$k] } } # Update the branding baseline (pre-fills + preserve-edits check) but leave the installer's # UI accent fixed — it doesn't follow the selected prompt theme. $s.Def = $newDef } # --- Step: Banner ----------------------------------------------------------------------- $stepBanner = { param($s, $i, $total) Write-PwshProfileStepHeader -Title 'Banner' -Index $i -Total $total -Accent $s.Accent -Code $s.Code ` -Body 'A large figlet banner printed once when the shell starts up — purely decorative.' # Show the current banner config, flagging anything off the theme default, then gate (default # No) before prompting. Recommended baseline is the current theme's branding ($s.Def). $shown = (@($s.Settings.Skip) -notcontains 'Banner') $rows = @([pscustomobject]@{ Label = 'Banner'; Value = $(if ($shown) { 'shown' } else { 'hidden' }); Recommended = 'shown' }) if ($shown) { $rows += [pscustomobject]@{ Label = 'Text'; Value = $s.Settings.BannerText; Recommended = $s.Def.BannerText } $rows += [pscustomobject]@{ Label = 'Color'; Value = $s.Settings.BannerColor; Recommended = $s.Def.BannerColor; Color = $true } $rows += [pscustomobject]@{ Label = 'Alignment'; Value = $s.Settings.BannerAlignment; Recommended = $s.Def.BannerAlignment } $rows += [pscustomobject]@{ Label = 'Font'; Value = $s.Settings.BannerFont; Recommended = $s.Def.BannerFont } } if (-not (Read-PwshProfileSettingChange -Message 'Change these banner settings?' -Row $rows -Accent $s.Accent)) { return } if (Read-SpectreConfirm -Message 'Show a startup banner?' -Color $s.Accent -DefaultAnswer 'y') { $s.Settings.Skip = @(@($s.Settings.Skip) | Where-Object { $_ -ne 'Banner' }) Write-PwshProfilePromptHelp 'The text drawn in the banner. `$env:` variables are expanded, so `$env:COMPUTERNAME` shows the machine name. Press Enter to keep the default shown; clear it to hide the banner entirely.' -Accent $s.Accent -Code $s.Code $s.Settings.BannerText = Read-SpectreText -Message 'Banner text (supports $env: variables, e.g. $env:COMPUTERNAME)' -DefaultAnswer $s.Settings.BannerText -AllowEmpty if ([string]::IsNullOrWhiteSpace($s.Settings.BannerText)) { # An empty banner text renders no banner at startup (Initialize-PwshProfile guards on # it), so treat a cleared text like a declined banner: restore the default text and # hide via -Skip Banner, rather than leaving a "shown but blank" half-state in the # review summary and generated call. Skip the remaining theming prompts. $s.Settings.BannerText = $s.Def.BannerText $s.Settings.Skip = @(@($s.Settings.Skip) + 'Banner' | Select-Object -Unique) return } Write-PwshProfilePromptHelp 'Color of the banner text — a Spectre color name (e.g. `Aqua`) or a hex value (e.g. `#c9aaff`).' -Accent $s.Accent -Code $s.Code $s.Settings.BannerColor = Read-SpectreText -Message 'Banner color (Spectre color name or hex)' -DefaultAnswer $s.Settings.BannerColor # Echo the chosen color as a swatch so the user sees what it looks like (Read-SpectreText # leaves the raw value on screen; this adds the colored preview beneath it). Guarded like the # other prompt-echo helpers so it no-ops when Spectre is unavailable. if (Get-Command Write-SpectreHost -ErrorAction SilentlyContinue) { Write-SpectreHost " [$($s.Accent)]✓[/] $(Format-PwshProfileColorValue $s.Settings.BannerColor)" } Write-PwshProfilePromptHelp 'Where the banner sits in the console width.' -Accent $s.Accent -Code $s.Code $s.Settings.BannerAlignment = Read-SpectreSelection -Message 'Banner alignment' -Color $s.Accent -Choices @('Left', 'Center', 'Right') Write-PwshProfilePromptAnswer $s.Settings.BannerAlignment -Accent $s.Accent # List the current font first so pressing Enter keeps it (selection menus can't pre-select). $fonts = @(Get-BundledFontName) $cur = $s.Settings.BannerFont if ($fonts -contains $cur) { $fonts = @($cur) + @($fonts | Where-Object { $_ -ne $cur }) } if ($fonts.Count -gt 0) { Write-PwshProfilePromptHelp 'The figlet (ASCII-art) typeface the banner text is rendered in.' -Accent $s.Accent -Code $s.Code $s.Settings.BannerFont = Read-SpectreSelection -Message 'Banner font' -Color $s.Accent -Choices $fonts -PageSize 10 -EnableSearch Write-PwshProfilePromptAnswer $s.Settings.BannerFont -Accent $s.Accent } } else { # No banner: disable it via -Skip Banner (deduped), leaving feature skips intact. $s.Settings.Skip = @(@($s.Settings.Skip) + 'Banner' | Select-Object -Unique) } } # --- Step: Step icon (always) ----------------------------------------------------------- $stepIcon = { param($s, $i, $total) Write-PwshProfileStepHeader -Title 'Step icon' -Index $i -Total $total -Accent $s.Accent -Code $s.Code ` -Body 'The little glyph printed in front of every startup step line (e.g. installing/initializing each tool).' $iconOptions = @( [pscustomobject]@{ Label = '🔩 Nut and bolt'; Icon = ':nut_and_bolt:' } [pscustomobject]@{ Label = '🌳 Deciduous tree'; Icon = ':deciduous_tree:' } [pscustomobject]@{ Label = '⚙️ Gear'; Icon = ':gear:' } [pscustomobject]@{ Label = '🔧 Wrench'; Icon = ':wrench:' } [pscustomobject]@{ Label = '🛠️ Hammer and wrench'; Icon = ':hammer_and_wrench:' } [pscustomobject]@{ Label = '🚀 Rocket'; Icon = ':rocket:' } [pscustomobject]@{ Label = '✨ Sparkles'; Icon = ':sparkles:' } [pscustomobject]@{ Label = '⭐ Star'; Icon = ':star:' } [pscustomobject]@{ Label = 'Custom shortcode…'; Icon = $null } ) # Float the current icon to the top and tag it so pressing Enter keeps it. $current = $iconOptions | Where-Object { $_.Icon -eq $s.Settings.StepIcon } | Select-Object -First 1 if ($current) { $current.Label += ' (current)' $iconOptions = @($current) + @($iconOptions | Where-Object { $_ -ne $current }) } $picked = Read-SpectreSelection -Message 'Step marker icon' -Color $s.Accent -Choices $iconOptions -ChoiceLabelProperty Label Write-PwshProfilePromptAnswer $picked.Label -Accent $s.Accent if ($null -eq $picked.Icon) { Write-PwshProfilePromptHelp 'A Spectre emoji shortcode wrapped in colons, e.g. `:gear:`. See `spectreconsole.net/emojis` for the full list.' -Accent $s.Accent -Code $s.Code $s.Settings.StepIcon = Read-SpectreText -Message 'Spectre emoji shortcode (e.g. ":gear:")' -DefaultAnswer $s.Settings.StepIcon } else { $s.Settings.StepIcon = $picked.Icon } } # --- Step: Features --------------------------------------------------------------------- $stepFeatures = { param($s, $i, $total) Write-PwshProfileStepHeader -Title 'Features' -Index $i -Total $total -Accent $s.Accent -Code $s.Code ` -Body 'Pick which startup features run — everything is checked by default, so uncheck to opt out. **oh-my-posh** always runs and has no checkbox.' $skip = @($s.Settings.Skip) # Current checked state per feature token (everything on unless previously skipped). $enabledMap = @{ PSReadLine = ($skip -notcontains 'PSReadLine') TerminalIcons = ($skip -notcontains 'TerminalIcons') PoshGit = ($skip -notcontains 'PoshGit') Zoxide = ($skip -notcontains 'Zoxide') Fzf = ($skip -notcontains 'Fzf') Fnm = ($skip -notcontains 'Fnm') Xh = ($skip -notcontains 'Xh') Completions = ($skip -notcontains 'Completions') } $selected = @(Read-PwshProfileFeatureTree -Enabled $enabledMap -Color $s.Accent -CodeColor $s.Code) # Anything unchecked becomes an individual -Skip token (Completions included — it runs as a # sub-step under Tools); keep the Banner skip (owned by the banner step). The wizard never # emits -SkipSection: unchecking a whole section in the tree just unchecks its leaves. $newSkip = @(@($skip | Where-Object { $_ -eq 'Banner' })) foreach ($t in 'PSReadLine', 'TerminalIcons', 'PoshGit', 'Zoxide', 'Fzf', 'Fnm', 'Xh', 'Completions') { if ($selected -notcontains $t) { $newSkip += $t } } $s.Settings.Skip = $newSkip $s.Settings.SkipSection = @() if ($selected -contains 'Zoxide') { Write-PwshProfilePromptHelp @( '**zoxide** is a smarter `cd`: it remembers the directories you visit most and lets you jump to one by a partial name — e.g. `cd dev` jumps straight to `C:\Dev` from anywhere.' 'This sets the command name you type to do that. The default `cd` replaces the built-in cd (normal paths still work, it just gains the jump trick).' 'Prefer `z` to leave the built-in cd untouched and add a separate `z` command (the zoxide convention). Press Enter to keep `cd`.' ) -Accent $s.Accent -Code $s.Code $s.Settings.ZoxideCommand = Read-SpectreText -Message "zoxide's jump command (replaces cd)" -DefaultAnswer $s.Settings.ZoxideCommand } } # --- Step: Nerd Font (optional) --------------------------------------------------------- $stepFonts = { param($s, $i, $total) Write-PwshProfileStepHeader -Title 'Nerd Font' -Index $i -Total $total -Accent $s.Accent -Code $s.Code ` -Body '**oh-my-posh** prompts use special icons (folder, git, OS glyphs) that only render in a "Nerd Font" — a normal font patched with those extra symbols.' Write-PwshProfilePromptHelp 'Say yes to install the recommended **Meslo** + **CascadiaCode** pair (then set one as your terminal font and the prompt renders right instead of showing boxes); no installs nothing. Downloads to your user profile; no admin needed.' -Accent $s.Accent -Code $s.Code $s.Settings.NerdFont = $null if (Read-SpectreConfirm -Message 'Install Nerd Fonts (Meslo + CascadiaCode) for the prompt glyphs? (download, no admin needed)' -Color $s.Accent -DefaultAnswer 'n') { # Ensure the NerdFonts module so its font catalog is queryable. Import-ModuleSafe NerdFonts if (Get-Command Get-NerdFont -ErrorAction SilentlyContinue) { $names = @(Get-NerdFont | Select-Object -ExpandProperty Name) # Meslo + CascadiaCode are the recommended pairing for oh-my-posh; keep only those # actually present in the catalog ("if possible"). $recommended = @('Meslo', 'CascadiaCode') | Where-Object { $names -contains $_ } if ($recommended.Count -gt 0) { $s.Settings.NerdFont = $recommended } else { Write-Warning 'Invoke-PwshProfileWizard: neither recommended font (Meslo, CascadiaCode) is in the NerdFonts catalog; skipping font install.' } } else { Write-Warning 'Invoke-PwshProfileWizard: the NerdFonts module is unavailable; skipping font installation.' } } } # --- Step: Winget settings -------------------------------------------------------------- $stepWinget = { param($s, $i, $total) Write-PwshProfileStepHeader -Title 'Winget' -Index $i -Total $total -Accent $s.Accent -Code $s.Code ` -Body 'Tunes the **winget** client itself — the defaults in its `settings.json` that apply whenever you install packages. Applied once now; pre-filled from your current winget settings.' # Show the current values (flagging any off the recommendation), then gate (default No) before # prompting. The current values are applied at install time either way. $rec = Get-WingetSettingRecommended $rows = @( [pscustomobject]@{ Label = 'Default scope'; Value = $s.Settings.WingetScope; Recommended = $rec.Scope } [pscustomobject]@{ Label = 'Progress bar'; Value = $s.Settings.WingetProgressBar; Recommended = $rec.ProgressBar } [pscustomobject]@{ Label = 'Anonymize paths'; Value = $(if ($s.Settings.WingetAnonymizePath) { 'on' } else { 'off' }); Recommended = $(if ($rec.AnonymizePath) { 'on' } else { 'off' }) } [pscustomobject]@{ Label = 'Install notes'; Value = $(if ($s.Settings.WingetDisableInstallNote) { 'suppressed' } else { 'shown' }); Recommended = $(if ($rec.DisableInstallNote) { 'suppressed' } else { 'shown' }) } ) if (-not (Read-PwshProfileSettingChange -Message 'Change these winget settings?' -Row $rows -Accent $s.Accent)) { return } # Default install scope — float the current value first so pressing Enter keeps it. Write-PwshProfilePromptHelp 'Whether `winget install` targets the current **user** (no admin prompt) or the whole **machine** by default. `user` is preferred and falls back to machine when a package has no per-user installer, so it never blocks an install.' -Accent $s.Accent -Code $s.Code $scopes = @('user', 'machine') if ($scopes -contains $s.Settings.WingetScope) { $scopes = @($s.Settings.WingetScope) + @($scopes | Where-Object { $_ -ne $s.Settings.WingetScope }) } $s.Settings.WingetScope = Read-SpectreSelection -Message 'Default install scope (winget)' -Color $s.Accent -Choices $scopes Write-PwshProfilePromptAnswer $s.Settings.WingetScope -Accent $s.Accent # Progress bar style — float the current value first. Write-PwshProfilePromptHelp 'The bar **winget** shows while downloading/installing: `rainbow` is a cycling gradient, `accent` a solid accent-color bar, `retro` a plain ASCII bar, `disabled` none.' -Accent $s.Accent -Code $s.Code $bars = @('accent', 'rainbow', 'retro', 'disabled') if ($bars -contains $s.Settings.WingetProgressBar) { $bars = @($s.Settings.WingetProgressBar) + @($bars | Where-Object { $_ -ne $s.Settings.WingetProgressBar }) } $s.Settings.WingetProgressBar = Read-SpectreSelection -Message 'Winget progress bar style' -Color $s.Accent -Choices $bars Write-PwshProfilePromptAnswer $s.Settings.WingetProgressBar -Accent $s.Accent # Anonymize displayed paths. Write-PwshProfilePromptHelp 'Replace known folders with their environment-variable names (e.g. `%LOCALAPPDATA%`) in **winget** output — handy for screenshots and screen-sharing.' -Accent $s.Accent -Code $s.Code $s.Settings.WingetAnonymizePath = [bool](Read-SpectreConfirm -Message 'Anonymize known paths in winget output?' -Color $s.Accent -DefaultAnswer $(if ($s.Settings.WingetAnonymizePath) { 'y' } else { 'n' })) # Suppress post-install notes. Write-PwshProfilePromptHelp 'Suppress the notes some packages print after a successful install, for quieter output.' -Accent $s.Accent -Code $s.Code $s.Settings.WingetDisableInstallNote = [bool](Read-SpectreConfirm -Message 'Suppress post-install notes?' -Color $s.Accent -DefaultAnswer $(if ($s.Settings.WingetDisableInstallNote) { 'y' } else { 'n' })) } # Ordered step table — drives both the forward pass and the review hub's Edit choices. The two # machine-setup steps (Nerd Fonts, Winget) lead; the prompt cosmetics follow. Theme must stay # ahead of Banner and Step icon, which pre-fill from the branding it seeds. $steps = [ordered]@{ 'Fonts' = $stepFonts 'Winget' = $stepWinget 'Theme' = $stepTheme 'Banner' = $stepBanner 'Step icon' = $stepIcon 'Features' = $stepFeatures } # Forward pass — thread each step's 1-based position and the total so its header shows "N of M". $keys = @($steps.Keys) $total = $keys.Count for ($n = 0; $n -lt $total; $n++) { & $steps[$keys[$n]] $state ($n + 1) $total } # --- Review hub ------------------------------------------------------------------------- # Color the values directly: known-safe slugs (theme/font/feature tokens) get a color tag, while # user-controlled text (banner text/color, custom path, icon shortcode) is escaped via $esc first # so it can never inject markup — then tinted. Labels stay bold. $accent = $state.Accent $code = $state.Code while ($true) { $set = $state.Settings $themeLine = if ($set.CustomTheme) { "custom: [$code]$(& $esc $set.CustomTheme)[/]" } else { "[$accent]$($set.Theme)[/]" } $bannerOff = (@($set.Skip) -contains 'Banner') $bannerLine = if ($bannerOff) { '[grey]off[/]' } else { "'$(& $esc $set.BannerText)' [grey]/[/] $(Format-PwshProfileColorValue $set.BannerColor) [grey]/[/] $($set.BannerAlignment) [grey]/[/] [$code]$($set.BannerFont)[/]" } $disabled = @(@($set.Skip) | Where-Object { $_ -ne 'Banner' }) + @($set.SkipSection) $featuresLine = if ($disabled.Count) { "all except [$code]$($disabled -join ', ')[/]" } else { '[grey]all enabled[/]' } $fontsLine = if (@($set.NerdFont).Count) { (@($set.NerdFont) | ForEach-Object { "[$accent]$_[/]" }) -join ', ' } else { '[grey]none[/]' } $anon = if ($set.WingetAnonymizePath) { 'on' } else { 'off' } $notes = if ($set.WingetDisableInstallNote) { 'off' } else { 'on' } $wingetLine = "scope [$accent]$($set.WingetScope)[/] [grey]·[/] bar [$accent]$($set.WingetProgressBar)[/] [grey]·[/] anon paths $anon [grey]·[/] install notes $notes" $summary = @( "[bold]Theme:[/] $themeLine" "[bold]Banner:[/] $bannerLine" "[bold]Step icon:[/] [$code]$(& $esc $set.StepIcon)[/]" "[bold]Features:[/] $featuresLine" "[bold]Nerd Fonts:[/] $fontsLine" "[bold]Winget:[/] $wingetLine" ) -join "`n" $summary | Format-SpectrePanel -Header '◆ Review your setup' -Border Rounded -Color $accent -Expand | Out-Host $submit = 'Submit — write the profile' $cancel = 'Cancel — exit without writing' $choices = @($submit) + @($keys | ForEach-Object { "Edit $_" }) + @($cancel) $pick = Read-SpectreSelection -Message 'What would you like to do?' -Color $accent -Choices $choices if ($pick -eq $submit) { break } if ($pick -eq $cancel) { return $null } $editName = $pick -replace '^Edit ', '' & $steps[$editName] $state ([array]::IndexOf($keys, $editName) + 1) $total } $state.Settings } |