# lib/menu.ps1 - the interactive menu, its pickers, and interactive preset editing. # # Part of claude-mode.ps1, which dot-sources it into its own script scope after # the settings at its top. Not meant to run on its own. ASCII only: Windows # PowerShell 5.1 reads a .ps1 without a BOM as ANSI. $PSScriptRoot here would be # lib\, so paths beside the main script go through $script:Here. # --------------------------------------------------------------------------- # Interactive menu (claude-mode with no arguments) # --------------------------------------------------------------------------- function Test-Interactive { try { if ([Console]::IsInputRedirected) { return $false } if ([Console]::IsOutputRedirected) { return $false } [void][Console]::WindowWidth return $true } catch { return $false } } # --------------------------------------------------------------------------- # UI frame - the screen region the interactive menus own. # # Successive selectors (mode -> preset, preset -> tier) must REPLACE each other # rather than stack, otherwise the earlier list stays on screen looking frozen # and interactive. The frame remembers where the current run of menus began; # each new selector wipes back to that line and redraws from it. # # Output that must persist (a switch summary, doctor results) calls Stop-UiFrame # first: that erases the menu, then lets the output print in the freed space and # stay there, with the next menu opening a fresh frame below it. # --------------------------------------------------------------------------- $script:UiActive = $false $script:UiTop = 0 $script:UiQuit = $false # set by the submenu to unwind the whole menu stack function Start-UiFrame { if ($script:UiActive) { return } try { $script:UiTop = [Console]::CursorTop } catch { $script:UiTop = 0 } $script:UiActive = $true } function Clear-UiFrame { if (-not $script:UiActive) { return } try { $w = [Math]::Max(1, [Console]::WindowWidth - 1) $blank = ' ' * $w $bottom = [Console]::CursorTop for ($y = $script:UiTop; $y -le $bottom; $y++) { [Console]::SetCursorPosition(0, $y) [Console]::Write($blank) } [Console]::SetCursorPosition(0, $script:UiTop) } catch { } } function Stop-UiFrame { Clear-UiFrame $script:UiActive = $false } # --------------------------------------------------------------------------- # Show-Select - arrow-key list picker. # # Draws the list once, then repaints it in place on every keypress by parking # the cursor back at the top of the region. Each row is padded to the console # width so a repaint erases whatever the previous, longer row left behind. # A fixed-height detail pane under the list shows the highlighted row's info, # which keeps the geometry constant - variable-height rows would make the # in-place repaint arithmetic fragile. # # Returns the selected index, or -1 if the user cancelled / the console cannot # support this (callers fall back to Read-Choice). # --------------------------------------------------------------------------- function Show-Select { param( [string] $Title, [string] $Status, [object[]] $Items, # each: Label (string), Detail (string[]) [int] $Default = 0, [int] $DetailLines = 3 ) if (-not (Test-Interactive)) { return -1 } $n = @($Items).Count if ($n -eq 0) { return -1 } $idx = [Math]::Max(0, [Math]::Min($Default, $n - 1)) $w = [Math]::Max(40, [Console]::WindowWidth - 1) # Take over the frame: a menu already on screen is wiped so this one lands # in its place instead of below it. if ($script:UiActive) { Clear-UiFrame } else { Start-UiFrame } $headerH = 3 Write-Host '' if ($Title) { $headerH = 4 $t = " $Title" if ($Status) { $t = $t.PadRight(34) + $Status } Write-Host $t -ForegroundColor Cyan } Write-Host ' up/down move enter select esc cancel' -ForegroundColor DarkGray Write-Host '' # Reserve the region first so the cursor never has to scroll mid-repaint, # then rewind to its top. $regionH = $n + 1 + $DetailLines for ($i = 0; $i -lt $regionH; $i++) { Write-Host '' } $top = [Console]::CursorTop - $regionH # Reserving may have scrolled the buffer, which moves everything up and # invalidates the remembered frame top. Re-anchor it from where the list # actually landed. $script:UiTop = [Math]::Max(0, $top - $headerH) # Truncate-or-pad a row to exactly the console width, so repainting a short # row fully erases a longer one underneath it. $pad = { param([string] $s) if ($s.Length -gt $w) { $s = $s.Substring(0, $w) } return $s.PadRight($w) } $cursorWasVisible = $true try { $cursorWasVisible = [Console]::CursorVisible; [Console]::CursorVisible = $false } catch { } try { while ($true) { [Console]::SetCursorPosition(0, $top) for ($i = 0; $i -lt $n; $i++) { $sel = ($i -eq $idx) $mark = if ($sel) { ' > ' } else { ' ' } $row = & $pad ($mark + $Items[$i].Label) if ($sel) { # An item may carry its own accent (mode rows do), so the # highlight itself tells you which provider you are on. $bg = 'Cyan' if ($Items[$i].PSObject.Properties.Name -contains 'Accent' -and $Items[$i].Accent) { $bg = [string]$Items[$i].Accent } Write-Host $row -ForegroundColor Black -BackgroundColor $bg } else { Write-Host $row -ForegroundColor Gray } } Write-Host (& $pad '') $detail = @($Items[$idx].Detail) for ($k = 0; $k -lt $DetailLines; $k++) { $line = if ($k -lt $detail.Count) { ' ' + $detail[$k] } else { '' } Write-Host (& $pad $line) -ForegroundColor DarkGray } # if/elseif rather than switch: `break`/`continue` inside a switch # nested in a loop is ambiguous in PowerShell, and this is the input # loop - it has to be unambiguous. $key = [Console]::ReadKey($true) $k = [string]$key.Key $ch = $key.KeyChar if ($k -eq 'UpArrow' -or $k -eq 'K') { $idx = ($idx - 1 + $n) % $n } elseif ($k -eq 'DownArrow' -or $k -eq 'J') { $idx = ($idx + 1) % $n } elseif ($k -eq 'Home') { $idx = 0 } elseif ($k -eq 'End') { $idx = $n - 1 } elseif ($k -eq 'Enter' -or $k -eq 'Spacebar') { return $idx } elseif ($k -eq 'Escape') { return -1 } elseif ($ch -eq 'q' -or $ch -eq 'Q') { return -1 } elseif ($ch -match '^[1-9]$' -and [int][string]$ch -le $n) { # Number keys still work as direct shortcuts. return ([int][string]$ch - 1) } } } finally { try { [Console]::SetCursorPosition(0, $top + $regionH) [Console]::CursorVisible = $cursorWasVisible } catch { } } } # Numeric fallback, used when the console cannot host Show-Select. function Read-Choice { param([string] $Prompt, [int] $Max, [string] $Default = '') while ($true) { $raw = Read-Host $Prompt if ([string]::IsNullOrWhiteSpace($raw)) { if ($Default) { return $Default } continue } $raw = $raw.Trim() if ($raw -eq 'q' -or $raw -eq '0') { return 'q' } $n = 0 if ([int]::TryParse($raw, [ref]$n) -and $n -ge 1 -and $n -le $Max) { return [string]$n } Write-Host ' (invalid choice)' -ForegroundColor DarkGray } } function Get-PresetSummaryLines { param($Preset) $out = @() if ($Preset.Contains('description')) { $out += [string]$Preset['description'] } $bits = @() foreach ($t in $script:Tiers) { if ($Preset['models'].Contains($t)) { $bits += "$t=$($Preset['models'][$t])" } } if ($bits.Count) { $out += ($bits -join ' ') } if ($Preset.Contains('contextTokens') -and $Preset['contextTokens']) { $out += ("context {0:N0} tokens base {1}" -f [int]$Preset['contextTokens'], $Preset['baseUrl']) } else { $out += "base $($Preset['baseUrl'])" } return $out } function Invoke-PresetPicker { param([string] $Provider) $names = @(Get-PresetNamesForProvider $Provider) if ($names.Count -eq 0) { throw "no presets defined for provider '$Provider'" } $default = $script:ProviderDefaultPreset[$Provider] if (-not ($names -contains $default)) { $default = $names[0] } $defaultIdx = [Math]::Max(0, [array]::IndexOf($names, $default)) $items = @() foreach ($n in $names) { $p = Get-Preset $n $label = $n if ($n -eq $default) { $label = "$n (default)" } $items += [pscustomobject]@{ Label = $label; Detail = (Get-PresetSummaryLines $p) } } $sel = Show-Select -Title "preset for $Provider" -Items $items -Default $defaultIdx if ($sel -ge 0) { return $names[$sel] } if ($sel -eq -1 -and (Test-Interactive)) { return $null } # cancelled # console too limited for the picker - fall back to numbers for ($i = 0; $i -lt $names.Count; $i++) { Write-Host (" {0}) {1}" -f ($i + 1), $names[$i]) } $c = Read-Choice -Prompt " preset [Enter = $default, q = cancel]" -Max $names.Count -Default 'D' if ($c -eq 'q') { return $null } if ($c -eq 'D') { return $default } return $names[[int]$c - 1] } # --------------------------------------------------------------------------- # Show-SearchSelect - arrow-key picker with a live type-to-filter box. # # Show-Select is fine for a handful of rows, but OpenRouter lists 300+ models, # which is unusable as a flat list. This keeps the same in-place repaint but adds # a filter line and a scrolling window over the matches. # # Returns the index into $Items, or -1 to cancel. # --------------------------------------------------------------------------- function Show-SearchSelect { param( [string] $Title, [string] $Status, [object[]] $Items, # each: Label (string), Detail (string[]), Key (string, searched) [string] $Query = '', [int] $MaxRows = 12, [int] $DetailLines = 2 ) if (-not (Test-Interactive)) { return -1 } $all = @($Items) if ($all.Count -eq 0) { return -1 } $w = [Math]::Max(40, [Console]::WindowWidth - 1) $MaxRows = [Math]::Max(3, [Math]::Min($MaxRows, [Console]::WindowHeight - 12)) if ($script:UiActive) { Clear-UiFrame } else { Start-UiFrame } $headerH = 3 Write-Host '' if ($Title) { $headerH = 4 $t = " $Title" if ($Status) { $t = $t.PadRight(34) + $Status } Write-Host $t -ForegroundColor Cyan } Write-Host ' type to filter up/down move enter select esc cancel' -ForegroundColor DarkGray Write-Host '' # filter line + blank + rows + count + blank + detail $regionH = 1 + 1 + $MaxRows + 1 + 1 + $DetailLines for ($i = 0; $i -lt $regionH; $i++) { Write-Host '' } $top = [Console]::CursorTop - $regionH $script:UiTop = [Math]::Max(0, $top - $headerH) $pad = { param([string] $s) if ($s.Length -gt $w) { $s = $s.Substring(0, $w) } return $s.PadRight($w) } $idx = 0 $off = 0 $cursorWasVisible = $true try { $cursorWasVisible = [Console]::CursorVisible; [Console]::CursorVisible = $false } catch { } try { while ($true) { # Filter on every keystroke. Match against Key when present so a row # can display extra decoration without breaking search. $q = $Query.Trim().ToLower() if ($q) { $matches = @($all | Where-Object { $hay = if ($_.PSObject.Properties.Name -contains 'Key' -and $_.Key) { [string]$_.Key } else { [string]$_.Label } $hay.ToLower().Contains($q) }) } else { $matches = $all } $m = $matches.Count if ($idx -ge $m) { $idx = [Math]::Max(0, $m - 1) } if ($idx -lt $off) { $off = $idx } if ($idx -ge $off + $MaxRows) { $off = $idx - $MaxRows + 1 } if ($off -gt [Math]::Max(0, $m - $MaxRows)) { $off = [Math]::Max(0, $m - $MaxRows) } [Console]::SetCursorPosition(0, $top) Write-Host (& $pad (" filter: " + $Query + "_")) -ForegroundColor White Write-Host (& $pad '') for ($r = 0; $r -lt $MaxRows; $r++) { $i = $off + $r if ($i -ge $m) { Write-Host (& $pad ''); continue } $sel = ($i -eq $idx) $mark = if ($sel) { ' > ' } else { ' ' } $row = & $pad ($mark + $matches[$i].Label) if ($sel) { Write-Host $row -ForegroundColor Black -BackgroundColor Cyan } else { Write-Host $row -ForegroundColor Gray } } $count = if ($m -eq 0) { ' (no match)' } else { " $($idx + 1) of $m" + $(if ($all.Count -ne $m) { " (filtered from $($all.Count))" } else { '' }) } Write-Host (& $pad $count) -ForegroundColor DarkGray Write-Host (& $pad '') $detail = if ($m -gt 0) { @($matches[$idx].Detail) } else { @() } for ($k = 0; $k -lt $DetailLines; $k++) { $line = if ($k -lt $detail.Count) { ' ' + $detail[$k] } else { '' } Write-Host (& $pad $line) -ForegroundColor DarkGray } $key = [Console]::ReadKey($true) $k = [string]$key.Key $ch = $key.KeyChar if ($k -eq 'UpArrow') { if ($m) { $idx = ($idx - 1 + $m) % $m } } elseif ($k -eq 'DownArrow') { if ($m) { $idx = ($idx + 1) % $m } } elseif ($k -eq 'PageUp') { $idx = [Math]::Max(0, $idx - $MaxRows) } elseif ($k -eq 'PageDown') { $idx = [Math]::Min([Math]::Max(0, $m - 1), $idx + $MaxRows) } elseif ($k -eq 'Home') { $idx = 0 } elseif ($k -eq 'End') { $idx = [Math]::Max(0, $m - 1) } elseif ($k -eq 'Enter') { if ($m -gt 0) { return [array]::IndexOf($all, $matches[$idx]) } } elseif ($k -eq 'Escape') { return -1 } elseif ($k -eq 'Backspace') { if ($Query.Length -gt 0) { $Query = $Query.Substring(0, $Query.Length - 1); $idx = 0; $off = 0 } } elseif ($ch -and [int][char]$ch -ge 32 -and [int][char]$ch -lt 127) { $Query += $ch; $idx = 0; $off = 0 } } } finally { try { [Console]::SetCursorPosition(0, $top + $regionH) [Console]::CursorVisible = $cursorWasVisible } catch { } } } # Catalogue per provider, shaped for the picker. Cached per process so opening # the picker repeatedly in one session does not refetch OpenRouter's 300+ models. $script:ModelCatalogueCache = @{} function Get-ModelChoices { param($Preset) $provider = [string]$Preset['provider'] $cacheKey = $provider + '|' + [string]$Preset['baseUrl'] if ($script:ModelCatalogueCache.ContainsKey($cacheKey)) { return $script:ModelCatalogueCache[$cacheKey] } $out = @() $kind = [string](Get-Provider $provider).catalogue.kind $tpl = $(if ($kind -eq 'lmstudio') { Get-LmStudioTemplateReport } else { @() }) foreach ($m in (Get-ProviderCatalogue $Preset | Sort-Object Id)) { $flag = '' if ($kind -eq 'openrouter') { $det = @(("context {0:N0} `$$($m.InM) in / `$$($m.OutM) out per 1M tokens" -f [int]$m.Ctx)) } elseif ($kind -eq 'lmstudio') { $det = @("state: $($m.State) max context: $($m.Ctx)") $short = ($m.Id -split '/')[-1].ToLower() $risk = $tpl | Where-Object { $_.Key -eq $short } | Select-Object -First 1 if ($risk -and $risk.Assertions.Count -gt 0) { $flag = ' [TEMPLATE RISK]' $det += 'chat template asserts message order - can break tool calls' } } else { $det = @([string]$m.Note) } $out += [pscustomobject]@{ Id = $m.Id; Key = $m.Id; Label = ($m.Id + $flag); Detail = $det } } $script:ModelCatalogueCache[$cacheKey] = $out return $out } function Read-ModelId { param($Preset, [string] $Tier, [string] $Current) $choices = @() $err = $null try { $choices = @(Get-ModelChoices $Preset) } catch { $err = $_.Exception.Message } if ($choices.Count -gt 0) { $items = @() # First row is always the manual escape hatch - a catalogue can lag # behind what the provider actually accepts. $items += [pscustomobject]@{ Id = $null; Key = 'type manually custom'; Label = ''; Detail = @('enter any model id by hand') } $items += $choices $sel = Show-SearchSelect -Title "model for '$Tier'" -Status "current: $Current" -Items $items if ($sel -lt 0) { return $null } if ($sel -gt 0) { return $items[$sel].Id } # fall through to manual entry } # A typed prompt cannot live inside the repainting frame - the redraw would # erase what is being typed. Close the frame first. Stop-UiFrame Write-Host '' if ($err) { Write-Warn2 "could not load the model list: $err" } Write-Host " current $Tier : $Current" -ForegroundColor DarkGray $val = Read-Host " new model id for '$Tier' (blank = cancel)" if ([string]::IsNullOrWhiteSpace($val)) { return $null } return $val.Trim() } # A blank preset per provider, with the endpoint/auth/context bits already right # so only the model choices are left to make. function New-PresetScaffold { param([string] $Provider) # Built from the provider's own template in providers.json; the POSIX # build's `cm-json.py scaffold` produces the same thing from the same file. $p = Get-Provider $Provider if (-not $p) { throw "unknown provider '$Provider'" } $tpl = $p.preset $base = [ordered]@{ provider = $Provider description = 'new preset' } $base['baseUrl'] = [string]$tpl.baseUrl $auth = [ordered]@{} if ($tpl.auth) { foreach ($prop in $tpl.auth.PSObject.Properties) { $auth[$prop.Name] = $prop.Value } } else { $auth['mode'] = 'vault'; $auth['keyRef'] = $Provider } $base['auth'] = $auth $base['models'] = [ordered]@{ opus = ''; sonnet = ''; haiku = ''; fable = '' } $base['subagentModel'] = 'inherit' $base['gatewayModelDiscovery'] = [bool]$tpl.gatewayModelDiscovery $base['contextTokens'] = $(if ($tpl.contextTokens) { [int]$tpl.contextTokens } else { 200000 }) if ($tpl.extraEnv) { $extra = [ordered]@{} foreach ($prop in $tpl.extraEnv.PSObject.Properties) { $extra[$prop.Name] = [string]$prop.Value } $base['extraEnv'] = $extra } return $base } function New-PresetInteractive { # 1. provider $provs = @($script:Modes | Where-Object { $_ -ne 'anthropic' }) $items = @() foreach ($p in $provs) { $items += [pscustomobject]@{ Label = $p; Detail = @($script:ModeLabel[$p]) } } $sel = Show-Select -Title 'new preset - which provider' -Items $items if ($sel -lt 0) { return } $provider = $provs[$sel] # 2. start from an existing preset of that provider, or blank $siblings = @(Get-PresetNamesForProvider $provider) $items = @([pscustomobject]@{ Label = ''; Detail = @("empty $provider preset - pick every model yourself") }) foreach ($n in $siblings) { $items += [pscustomobject]@{ Label = "copy of $n"; Detail = (Get-PresetSummaryLines (Get-Preset $n)) } } $sel = Show-Select -Title 'start from' -Items $items if ($sel -lt 0) { return } $preset = if ($sel -eq 0) { New-PresetScaffold $provider } else { Get-Preset $siblings[$sel - 1] } # 3. name it Stop-UiFrame Write-Host '' Write-Host " new $provider preset" -ForegroundColor Cyan $name = Read-Host ' name (letters, digits, dash; blank = cancel)' if ([string]::IsNullOrWhiteSpace($name)) { return } $name = $name.Trim() if ($name -notmatch '^[A-Za-z0-9][A-Za-z0-9._-]*$') { Write-Err2 "invalid name '$name' - use letters, digits, dash, dot, underscore" return } if (Test-Path -LiteralPath (Get-PresetPath $name)) { Write-Err2 "preset '$name' already exists" return } $preset['description'] = if ($sel -eq 0) { "custom $provider preset" } else { "copy of $($siblings[$sel - 1])" } Write-JsonFile (Get-PresetPath $name) $preset Write-Ok "created preset '$name' ($provider)" # 4. straight into editing it Invoke-PresetEditor -Name $name } function Invoke-PresetEditor { param([string] $Name) if (-not $Name) { $names = @(Get-PresetNames) if ($names.Count -eq 0) { Write-Warn2 'no presets'; return } $items = @() foreach ($n in $names) { $p = Get-Preset $n $items += [pscustomobject]@{ Label = ("{0,-18} [{1}]" -f $n, $p['provider']) Detail = (Get-PresetSummaryLines $p) } } $sel = Show-Select -Title 'edit which preset' -Items $items if ($sel -lt 0) { return } $Name = $names[$sel] } $name = $Name $preset = Get-Preset $name while ($true) { $rows = @() foreach ($t in $script:Tiers) { $cur = '' if ($preset['models'].Contains($t)) { $cur = [string]$preset['models'][$t] } $rows += [pscustomobject]@{ Tier = $t; Current = $cur } } $sub = '' if ($preset.Contains('subagentModel')) { $sub = [string]$preset['subagentModel'] } $rows += [pscustomobject]@{ Tier = 'subagent'; Current = $sub } $items = @() foreach ($r in $rows) { $items += [pscustomobject]@{ Label = ("{0,-9} {1}" -f $r.Tier, $r.Current) Detail = @("change which model backs the '$($r.Tier)' tier", "current: $($r.Current)") } } $c = Show-Select -Title "$name [$($preset['provider'])]" -Status 'esc = done' -Items $items if ($c -lt 0) { return } $tier = $rows[$c].Tier $val = Read-ModelId -Preset $preset -Tier $tier -Current $rows[$c].Current if (-not $val) { continue } # Set-PresetTier may re-apply the live preset, which prints a full switch # summary. Drop the frame so that output survives instead of being wiped # by the next redraw of the tier list. Stop-UiFrame Set-PresetTier -Name $name -Tier $tier -Model $val $preset = Get-Preset $name } } # The second level: everything that is not switching mode. Loops until the user # backs out, so running doctor then editing a preset costs one trip in. function Invoke-MoreMenu { while ($true) { $state = Get-State $current = [string]$state['mode'] $curDesc = $current if ($current -ne 'anthropic' -and $state['preset']) { $curDesc = "$current / $($state['preset'])" } $items = @( [pscustomobject]@{ Label = 'status'; Detail = @('show the full active configuration') } [pscustomobject]@{ Label = 'edit presets'; Detail = @('pick models per tier from the provider catalogue') } [pscustomobject]@{ Label = 'new preset'; Detail = @('create a preset - blank or copied from an existing one') } [pscustomobject]@{ Label = 'doctor'; Detail = @('verify auth, endpoint, model ids, context window') } [pscustomobject]@{ Label = 'back'; Detail = @('return to the mode menu') } [pscustomobject]@{ Label = 'quit'; Detail = @() } ) $sel = Show-Select -Title 'claude-mode - more' -Status "currently: $curDesc" -Items $items if ($sel -lt 0) { return } # esc = back switch ($sel) { 0 { Stop-UiFrame; Invoke-Status } # output worth keeping 1 { Invoke-PresetEditor } # more menus - keep the frame 2 { New-PresetInteractive } 3 { Stop-UiFrame; Invoke-Doctor } 4 { return } 5 { Stop-UiFrame; $script:UiQuit = $true; return } } } } function Invoke-Menu { if (-not (Test-Interactive)) { Invoke-Status; return } $script:UiQuit = $false # Drawn once, above the frame anchor, so the menus repaint underneath it and # the banner stays put instead of flickering on every keypress. $st0 = Get-State Show-Banner -Mode ([string]$st0['mode']) -Preset $(if ([string]$st0['mode'] -ne 'anthropic') { [string]$st0['preset'] } else { '' }) while ($true) { $state = Get-State $current = [string]$state['mode'] $curDesc = $current if ($current -ne 'anthropic' -and $state['preset']) { $curDesc = "$current / $($state['preset'])" } # The mode you are already in is not offered - nothing to switch to. $choices = @($script:Modes | Where-Object { $_ -ne $current }) $items = @() foreach ($m in $choices) { $detail = @($script:ModeLabel[$m]) try { $pn = $script:ProviderDefaultPreset[$m] if ($pn -and (Test-Path -LiteralPath (Get-PresetPath $pn))) { $detail += (Get-PresetSummaryLines (Get-Preset $pn))[1] } } catch { } $items += [pscustomobject]@{ Label = ("switch to " + $m) Detail = $detail Accent = $script:ModeColor[$m] } } # Everything that is not "switch mode" lives one level down, so the three # things this tool exists to do are the whole first screen. $items += [pscustomobject]@{ Label = 'more ...'; Detail = @('status, presets, doctor') } $sel = Show-Select -Title 'claude-mode' -Status "currently: $curDesc" -Items $items if ($sel -lt 0) { Stop-UiFrame; return } $nChoices = $choices.Count if ($sel -eq $nChoices) { Invoke-MoreMenu if ($script:UiQuit) { return } continue } $mode = $choices[$sel] if ($mode -eq 'anthropic') { Stop-UiFrame; Set-ClaudeMode -Mode 'anthropic' -PresetName ''; return } $preset = Invoke-PresetPicker -Provider $mode if (-not $preset) { continue } Stop-UiFrame Set-ClaudeMode -Mode $mode -PresetName $preset return } }