|
<# |
|
|
|
BananaAcid 2022-2026 |
|
License MIT |
|
v2.0.0 |
|
|
|
To run this file and get PowerShellGSM installed, paste the following into your powershell 5 (not powershell 7+ [posh.exe]): |
|
|
|
|
|
Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://gist.githubusercontent.com/bananaacid/1dc9117571967b26ceabc972009137ae/raw/install_PowerShellGSM.ps1')) |
|
|
|
|
|
|
|
Changes: |
|
- 21.08.2026, 2.0.0 added newer Menu function, some coloring, some fixes to fully work with PS 5+ and 7+ |
|
- 14.06.2024, 1.1.0 added version selection for PowershellGSM |
|
- 16.02.2024, 1.0.3 fix for PalWorld config: parsed the replacement block (Sart-ServerProp) as well |
|
#> |
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingCmdletAliases', '', Scope = 'Function', Target = '*')] # allow aliases in linter |
|
[CmdletBinding()] # allow -Debug |
|
Param() # no parameters, but required |
|
|
|
Function Main() { |
|
# Just to make sure. For me it was optional. |
|
Add-Type -Assembly System.IO.Compression.FileSystem |
|
|
|
|
|
|
|
# GIT head URL |
|
$url = "https://github.com/patrix87/PowerShellGSM/archive/refs/heads/main.zip" |
|
$tmpFile = "$env:TEMP\PowerShellGSM.main.zip"; |
|
|
|
# GIT releases JSON endpoint URL: |
|
$releasesUrl = "https://api.github.com/repos/patrix87/PowerShellGSM/releases" |
|
|
|
|
|
|
|
$lastState = Get-Content ~\.PowerShellGSM -erroraction 'silentlycontinue' | ConvertFrom-Json -erroraction 'silentlycontinue' |
|
|
|
if ( -not $? ) { |
|
$lastState = [pscustomobject]@{ |
|
exPath = ""; |
|
lastVersion = "main" |
|
} |
|
} |
|
else { |
|
## MIGRATION from v1.0.3 |
|
if ($null -eq $lastState.lastVersion) { |
|
$newState = [pscustomobject]@{ |
|
exPath = $lastState.exPath; |
|
lastVersion = "main" |
|
} |
|
$lastState = $newState |
|
} |
|
|
|
|
|
echo "PowershellGSM Installation (Version $(ConvertTo-CreateMenuAnsiColorString -InputString $lastState.lastVersion -ForegroundColor Green)) found in: $(ConvertTo-CreateMenuAnsiColorString -InputString $lastState.exPath -ForegroundColor Blue)" |
|
} |
|
|
|
|
|
## DOWNLOAD |
|
|
|
if (Test-Path ($tmpFile -Replace '.main.',".$($lastState.lastVersion).") ) { |
|
$r = Read-Host "Download fresh PowerShellGSM files (Y/n)" |
|
if ($r -ne "" -and $r -ne "y" -and $r -ne "Y") { |
|
$noDl = $True |
|
} |
|
} |
|
|
|
if ($noDl -ne $True) { |
|
|
|
|
|
## SELECT VERSION |
|
|
|
# get the github releases content |
|
$releases = Invoke-RestMethod -Uri $releasesUrl |
|
|
|
# prepare version selection |
|
$MenuOptions = @("[no update]", "cutting edge") + ($releases |% { $_.tag_name }) |
|
|
|
# ask the user to select a version |
|
$releaseVersionSelection = New-SelectionMenu -Title "`nSelect PowerShellGSM version to install:`n" -MenuOptions $MenuOptions -Columns "auto" -Selected 2 # -CleanHost $True |
|
$releaseVersion = $MenuOptions[$releaseVersionSelection] |
|
|
|
if ($releaseVersion -eq "[no update]") { |
|
$noDl = $True; |
|
} |
|
elseif (($releaseVersion -eq "cutting edge") -or ($null -eq$releaseVersion)) { |
|
# download head as previous |
|
$releaseVersion = "main" |
|
} |
|
else { |
|
# get the url of the new |
|
$url = $releases |? { $_.tag_name -eq $releaseVersion } |% { $_.zipball_url } |
|
$tmpFile = $tmpFile -Replace '.main.',".$releaseVersion." |
|
} |
|
|
|
|
|
|
|
## DOWNLOAD |
|
if ($noDl -ne $True) { |
|
echo "downloading $releaseVersion from $url" |
|
Invoke-WebRequest $url -OutFile $tmpFile |
|
|
|
# get the folder of the element |
|
$zipSubFolder = [IO.Compression.ZipFile]::OpenRead($tmpFile).Entries.FullName |? { $_ -match [regex]"patrix87-PowerShellGSM-[^\/]+\/$" } |select -first 1 |
|
|
|
|
|
## EXTRACT |
|
|
|
if ($lastState.exPath -ne "") { $def = "to " + $lastState.exPath } else { $def = "here" } |
|
|
|
$r = Read-Host "Extract installation file $def (Y/n)" |
|
if ($r -eq "" -or $r -eq "y" -or $r -eq "Y") { |
|
if ($lastState.exPath -ne "") { $r = $lastState.exPath } else { $r = pwd } |
|
} |
|
else { |
|
$r = Read-Host "Installation path" |
|
} |
|
$exPath = $r |
|
|
|
echo "Installation path used: $exPath" |
|
|
|
Expand-Archive $tmpFile $env:TEMP -Force |
|
|
|
|
|
ni $exPath -ItemType Directory -Force >$null |
|
cp "$env:TEMP\$zipSubFolder\*" $exPath\ -Force -R |
|
rm "$env:TEMP\$zipSubFolder" -Force -R |
|
|
|
$lastState.lastVersion = $releaseVersion |
|
} |
|
else { |
|
$exPath = $lastState.exPath |
|
} |
|
|
|
} |
|
else { |
|
if ($lastState.exPath -ne "") { $def = $lastState.exPath } else { $def = "here" } |
|
$r = Read-Host "Enter path to previous installation (default: $def)" |
|
if ($r -eq "") { |
|
if ($lastState.exPath -ne "") { $r = $lastState.exPath } else { $r = pwd } |
|
} |
|
|
|
$exPath = $r |
|
} |
|
|
|
|
|
## ACTIVATE TEMPLATE |
|
|
|
# check if path is a reparse point: Symlink/Hardlink |
|
function Test-ReparsePoint([string]$path) { |
|
$file = Get-Item $path -Force -ea SilentlyContinue |
|
return [bool]($file.Attributes -band [IO.FileAttributes]::ReparsePoint) |
|
} |
|
|
|
$Configs = ls $exPath\configs |? {$_.Name -notlike '#*' -and $_.Name -notlike '*.backup.*'} |% BaseName |
|
Write-Host "Existing configs:" -ForegroundColor Yellow |
|
$list = ConvertTo-CreateMenuAnsiColorString -InputString $($Configs -join ", ") -ForegroundColor Green |
|
echo "$list`n" |
|
|
|
|
|
$Options = ls $exPath\templates |? {$_.Name -notlike '#*'} |% BaseName |
|
$Options = @("[no template]") + $Options |
|
|
|
$OptionsCaptions = $Options |% { $_ -in $Configs ? "·$_" : $_ } |
|
|
|
$Selection = New-SelectionMenu -Title "Select a server template to add or edit:`n" -MenuOptions $OptionsCaptions -Columns "auto" -MaximumColumnWidth 20 -ShowCurrentSelection $True |
|
$templateName = $Options[$Selection] |
|
$cfgFile = "$templateName.psm1" |
|
$cfgFileBackup = $null |
|
|
|
if ($Selection -eq 0) { |
|
continue |
|
} |
|
|
|
if (Test-Path $exPath\configs\$cfgFile) { |
|
$r = Read-Host "Overwrite the config file for the selected server with new and blank one (backup will be created) (y/N)" |
|
if ($r -eq "" -or $r -eq "n" -or $r -eq "N") { |
|
$ignoreReplace = $True |
|
} |
|
else { |
|
# just to make sure ... |
|
$timestamp = Get-Date -format "yyyy-MM-dd_HH-mm-ss" |
|
cp "$exPath\configs\$cfgFile" "$exPath\configs\$cfgFile.backup.$timestamp" -Force |
|
$cfgFileBackup = "$exPath\configs\$cfgFile.backup.$timestamp" |
|
|
|
if (-not (Test-ReparsePoint "$exPath\launchers\$templateName.cmd")) { |
|
cp "$exPath\launchers\$templateName.cmd" "$exPath\launchers\$templateName.cmd.backup.$timestamp" |
|
} |
|
} |
|
} |
|
|
|
if ($ignoreReplace -ne $True) { |
|
cp "$exPath\templates\$cfgFile" "$exPath\configs" -Force |
|
|
|
if ($PSVersionTable.PSVersion.Major -ge 5) { |
|
# use symlink, if possible |
|
New-Item -Path "$exPath\launchers\$templateName.cmd" -ItemType SymbolicLink -Value "$exPath\launchers\run.cmd" -Force | Out-Null |
|
} |
|
else { |
|
cp "$exPath\launchers\run.cmd" "$exPath\launchers\$templateName.cmd" -Force |
|
} |
|
} |
|
|
|
Write-Host "To run the selected server for $templateName, run $exPath\launchers\$templateName.cmd`n" -ForegroundColor Yellow |
|
|
|
|
|
## change passwords |
|
|
|
$r = Read-Host "Change all passwords in the new config (Y/n)" |
|
if ($r -eq "" -or $r -eq "y" -or $r -eq "Y") { |
|
|
|
function C1($str) { |
|
return ConvertTo-CreateMenuAnsiColorString -InputString $str -ForegroundColor blue |
|
} |
|
function C2($str) { |
|
return ConvertTo-CreateMenuAnsiColorString -InputString $str -ForegroundColor green |
|
} |
|
|
|
# get content |
|
$inputFile = gc $exPath\configs\$cfgFile -Raw |
|
|
|
# find description, key, value (password) |
|
$results = [Regex]::Matches($inputFile, "^[\s]+(?<desc>#.*)*\n^[\s]+(?<line>(?<key>[^#'\s]*Password[^=\s]*)[\s]*=[\s]*""(?<val>[^""]*)"")", "IgnoreCase,Multiline") |
|
|
|
$newFile = $inputFile |
|
|
|
For ($i = 0; $i -lt $results.count; $i++) { |
|
|
|
# ask for new password and show comment + old value |
|
Write-Host "$($results[$i].Groups['desc'].Value) -- currently: '$($results[$i].Groups['val'].Value)'" -ForegroundColor DarkGray |
|
$val = Read-Host "New password for '$(C1( $results[$i].Groups['key'].Value) )' (empty for random)" |
|
|
|
if ($val -eq "") { |
|
$val = ((-join (((48..57)+(65..90)+(97..122)) * 80 |Get-Random -Count 8 |%{[char]$_})) -replace '0|O|o|I|l|1|5|S|s','A').ToLower() # if not toLower, add: |Y|y|V|v|K|k|W|w|P|p|C|c|X|x |
|
Write-Host "... random: $(C2($val))" |
|
} |
|
# replace old value in line |
|
$a = '"' + $results[$i].Groups["val"].Value + '"' |
|
$b = '"' + $val + '"' |
|
$x = $results[$i].Groups["line"].Value -replace $a,$b |
|
|
|
# replace old row |
|
$newFile = $newFile -replace $results[$i].Groups["line"].Value,$x |
|
} |
|
|
|
Write-Host "" |
|
|
|
# write new content |
|
$newFile | Out-File $exPath\configs\$cfgFile |
|
} |
|
|
|
|
|
## OPEN GAME CONFIGS |
|
$gameConfigFolderPath = gc $exPath\configs\$cfgFile | Select-String -Pattern "ConfigFolder\s*=\s*`"(?<path>.*)`"" |% { $_.Matches[0].Groups['path'].Value } |
|
$gameConfigFolderPath = $gameConfigFolderPath -ireplace '\$Name',$templateName |
|
$gameConfigFolderPath = [System.IO.Path]::GetFullPath((Join-Path $PWD $gameConfigFolderPath)) # path does not need to exist |
|
Write-Host "The game config folder is: $gameConfigFolderPath" -ForegroundColor Yellow |
|
if (Test-Path $gameConfigFolderPath) { |
|
$r = Read-Host "Show game config folder (Y/n)`n" |
|
if ($r -eq "" -or $r -eq "y" -or $r -eq "Y") { |
|
explorer "$gameConfigFolderPath" |
|
} |
|
} |
|
|
|
Write-Host "" |
|
|
|
## SHOW CONFIG |
|
$r = Read-Host "Show config file to be edited manually (Y/n)" |
|
if ($r -eq "" -or $r -eq "y" -or $r -eq "Y") { |
|
notepad "$exPath\configs\$cfgFile" |
|
Start-Process -FilePath explorer.exe -ArgumentList "/select,`"$exPath\configs\$cfgFile`"" |
|
} |
|
|
|
|
|
## SHOW FOLDER |
|
$r = Read-Host "Show installation folder (y/N)" |
|
if ($r -eq "y" -or $r -eq "Y") { |
|
explorer "$exPath" |
|
} |
|
|
|
|
|
## SHOW WEBSITE |
|
$r = Read-Host "Show PowershellGSM Readme (y/N)" |
|
if ($r -eq "y" -or $r -eq "Y") { |
|
start "https://github.com/patrix87/PowerShellGSM/blob/main/README.md" |
|
} |
|
|
|
Write-Host "" |
|
|
|
## RUN? |
|
$r = Read-Host (ConvertTo-CreateMenuAnsiColorString -InputString "Install/run the selected server for $templateName (y/N)" -ForegroundColor Red) |
|
if ($r -eq "y" -or $r -eq "Y") { |
|
echo "$exPath\launchers\$templateName.cmd" |
|
start "$exPath\launchers\$templateName.cmd" -WorkingDirectory "$exPath\launchers\" |
|
} |
|
|
|
|
|
## REMEMBER |
|
|
|
$lastState.exPath = Convert-Path $exPath |
|
ConvertTo-Json $lastState | Out-File ~\.PowerShellGSM |
|
} |
|
|
|
|
|
|
|
|
|
## MODULES |
|
|
|
# https://raw.githubusercontent.com/BananaAcid/CreateMenuPS/refs/tags/2.0.2/Create-Menu/Create-Menu.ps1 |
|
Function New-SelectionMenu { |
|
<# |
|
.SYNOPSIS |
|
Shows strings as a table to be selectable by navigating with arrow keys |
|
|
|
.DESCRIPTION |
|
Author: Nabil Redmann (BananaAcid) |
|
License: ISC |
|
|
|
.INPUTS |
|
array of strings |
|
|
|
.PARAMETER MenuOptions |
|
Value: <String[]> |
|
|
|
Takes an array with selections (must be more then one) |
|
.PARAMETER Title |
|
Value: <Null|ScriptBlock|String> |
|
|
|
Takes a string or a scriptblock, use $global:varname to link to Title, Footer or SelectionCallback (available vars: $Selection, $SelectionValue, $MenuOptions, $MenuOptionsInput, $esc, $CL, $global:*) |
|
.PARAMETER Selected |
|
Value: <Null|Integer> |
|
|
|
Initial string to select |
|
.PARAMETER Footer |
|
Value: <Null|ScriptBlock|String> |
|
|
|
Takes a string or a scriptblock (available vars: $Selection, $SelectionValue, $MenuOptions, $MenuOptionsInput, $esc, $CL, $global:*) |
|
.PARAMETER SelectionCallback |
|
Value: <Null|ScriptBlock> |
|
|
|
If you want to trigger something on selection or a key, or change the $MenuOptions/$Selection, return $False to exit |
|
|
|
# params in: $Selection, $SelectionValue, $MenuOptions, $MenuOptionsInput, $KeyInput, $esc (char 27, ansi seq. start), $CL (ansicode to clear to end of line) |
|
# return: $False to end, or array of new options and triggers re-calculation of the menu |
|
# Modifiable: $Selection to change the selection, $MenuOptions for new array of options (like returning an array, BUT DOES NOT re-calculate the menu) |
|
|
|
.PARAMETER Columns |
|
Value: <"Auto"|Integer> |
|
|
|
Define how many columns should be shown (default: "Auto") |
|
.PARAMETER MaximumColumnWidth |
|
Value: <"Auto"|Integer> |
|
|
|
The maximum amount of chars in a cell should be displayed, if to large, '...' will be appended |
|
(default: "Auto" if Columns is a number, results in "20" if Columns is "Auto") |
|
.PARAMETER ShowCurrentSelection |
|
Value: <Boolean> |
|
Shows the current selection text in full length in the console title (default: $False) |
|
.PARAMETER PassThrou |
|
Without, will output the index of the selection, otherwise the selected string (default: $False) |
|
.PARAMETER ReturnObject |
|
Returns Selection index, SelectionValue string, MenuOptions string[] of maybe modified items, MenuOptionsInput string[] of input strings, Items object of {"Name" maybe modified,"Index","Input" originial string} -- has a higher priority then PassThrou |
|
.PARAMETER ForegroundColor |
|
Value: <[Console]::ForegroundColor|ConsoleColor> |
|
Color for the selection (default: Black) |
|
.PARAMETER ForegroundColorSelection |
|
Value: <Black|ConsoleColor> |
|
|
|
Color for the selection (default: Black) |
|
.PARAMETER BackgroundColorSelection |
|
Value: <Cyan|ConsoleColor> |
|
|
|
Color for the selection (default: Cyan) |
|
.PARAMETER ForegroundColorTitle |
|
Value: <Cyan|ConsoleColor> |
|
|
|
Color for the title (default: Cyan) |
|
.PARAMETER ForegroundColorFooter |
|
Value: <Black|ConsoleColor> |
|
|
|
Color for the footer (default: Black) |
|
|
|
.PARAMETER ClearHost |
|
Value: <Boolean> |
|
|
|
Will clear the screen on start and after selecting from the terminal (default: False) |
|
|
|
.PARAMETER CleanHost |
|
Value: <Boolean> |
|
|
|
Will clear the menu after selecting from the terminal (default: False) |
|
|
|
.PARAMETER FilterCallback |
|
Value: <Null|ScriptBlock> |
|
|
|
Will allows to modify the list of strings before they are shown |
|
|
|
# params in: $CellValue, $Current, $CurrentValue, $Selection, $SelectionValue, $MenuOptions, $MenuOptionsInput, $esc, $CL, $Row, $Column |
|
# $esc = char 27, ansi seq. start |
|
# $CL = ansi seq to clear to end of line |
|
# $CellValue = the cell content, the $SelectedValue with spaces around it |
|
# return: new value for $CellValue (if nothing/$false/$null is returned, it will not be changed) |
|
# Modifiable: $MenuOptions,$Selection,$CellValue |
|
|
|
.OUTPUTS |
|
Default is the index of the selected string, using -PassThrou it will be the selected string, using -ReturnObject it will be an object with items |
|
|
|
ReturnObject = { |
|
Selection = index of the selection |
|
SelectionValue = string of the selection |
|
|
|
MenuOptions = string[] of maybe modified items (was used for display) |
|
MenuOptionsInput = string[] of input strings |
|
|
|
Items = { |
|
Name = maybe modified (was used for display) |
|
Index = index of the item within the MenuOptions (Not MenuOptionsInput) |
|
Input = originial string |
|
} |
|
} |
|
|
|
.EXAMPLE |
|
import-module ./create-menu.ps1 |
|
ls | Create-Menu |
|
show all files as a selection and return its index upon selecting |
|
.EXAMPLE |
|
New-Module -Name "Create Menu" -ScriptBlock ([Scriptblock]::Create((New-Object System.Net.WebClient).DownloadString("https://gist.githubusercontent.com/BananaAcid/b8efca90cc6ca873fa22a7f9b98d918a/raw/Create-Menu.ps1"))) | Out-Null |
|
ls | Create-Menu |
|
show all files as a selection and return its index upon selecting. Loading from remote location. |
|
.EXAMPLE |
|
$check = Create-Menu no,yes "Want it?" 1 |
|
Shortest version. "no" first, becuase index 0 is equal to false. the "1" selects index 1 initially |
|
|
|
Outputs on single line: `Want it? no yes ` |
|
.EXAMPLE |
|
$check = Create-Menu @("no","yes") -Title "Want it?" -Selected 1 |
|
Longer version |
|
.EXAMPLE |
|
ls | create-menu -passthrou -T "Show Content:`n" |% {cat $_} |
|
Outputs a filename's contents after selecting |
|
.EXAMPLE |
|
echo "Select a letter"; $sel = @("a","b","c") | Create-Menu -PassThrou; echo "Selected: $sel" |
|
Usage for -MenuOptions by piping in |
|
.EXAMPLE |
|
ls ../ | Create-Menu -Title "abc`n-----" |
|
A simple string as title |
|
.EXAMPLE |
|
ls ../ | Create-Menu -Title {"SEL: $SelectionValue`n-----`n"} |
|
A scriptblock with an internal variable |
|
.EXAMPLE |
|
ls ../ | Create-Menu -Title {"SEL: $SelectionValue`n-----`n"} -ReturnObject |
|
A scriptblock with an internal variable, returning the selection opens |
|
.EXAMPLE |
|
ls ../ | Create-Menu -Title {Write-Host Green "SEL: $SelectionValue`n-----`n"} |
|
A scriptblock with colored title |
|
.EXAMPLE |
|
Create-Menu a,b -SelectionCallback {if ($KeyInput -eq 27) { return $False } } |
|
ESC to cancel input |
|
.EXAMPLE |
|
Create-Menu 0,1,2,3 -t {"SEL: $global:ki`n-----"} -SelectionCallback { $global:ki = $KeyInput } |
|
Show code of pressed key |
|
.EXAMPLE |
|
$YesNoCB = { If ($KeyInput -eq 89) { $Selection = 0 ; Return $True } If ($KeyInput -eq 78) { $Selection = 1 ; Return $True } } |
|
Create-Menu Y,n -SelectionCallback $YesNoCb |
|
Show "Y" and "n", pressing y (89) or n (78) will select the specific index, exit and show the selected |
|
|
|
.EXAMPLE |
|
$SpacePressed = { |
|
If ($KeyInput -eq 32) { # space-key |
|
if ($SelectionValue -like '`* *') { # check if item has a '* ' prefix = is selected already |
|
$MenuOptions[$Selection] = $MenuOptionsInput[$Selection] # remove prefix by setting it to the original string |
|
} Else { |
|
$MenuOptions[$Selection] = '* ' + $SelectionValue # add prefix to displayed item |
|
} |
|
} |
|
} |
|
$ret = ls ~/ | Create-Menu -t {"Full Name: $SelectionValue`n-----`n"} -SelectionCallback $SpacePressed -MaximumColumnWidth 40 -ReturnObject |
|
$selected = $ret.Items |? { $_.Name -like '`* *' } |% Input # check all items, if they had been marked (would also work: .Name -ne .Input) |
|
Write-Host "File Paths: ", $selected |
|
|
|
Allows to select multiple files with the space key (Keycode 32), then gets their names |
|
|
|
|
|
.EXAMPLE |
|
$names = "Tom", "Tim", "John", "Alice", "Bob", "Eve", "Adam", "Sarah", "Michael", "Jessica", "William", "Oliver", "Benjamin", "Hannah", "Kevin", "Lily", "David", "Emily", "Matthew", "Ashley", "Joseph", "James", "Laura", "Robert", "Richard", "Patricia", "Christopher", "Nicolas", "Sam", "Jennifer", "Lisa", "Brian", "Heather", "Katherine", "Julia", "Steven", "Amanda", "Rebecca", "Linda", "Daniela", "Elizabeth", "Andrew", "Stephanie", "Anthony", "Rachel", "Michelle", "Joshua", "Samantha", "Emi", "Alex", "Steven", "Amanda", "Rebecca", "Linda", "Daniel", "Elizabeth", "André", "Stephanie", "Anthony", "Rachel", "Michelle", "Joshua", "Sammy", "Amy", "Alexander", "Sammy" |
|
[Collections.ArrayList]$Global:Selected = @() # collect indexes of selected names |
|
Create-Menu $names ` |
|
-Title { "Selected Idx: $Global:selected`n-----`n" } ` |
|
-Footer { "Selected Names: $( $Global:selected |% { $MenuOptionsInput[$_] } )" } ` |
|
-SelectionCallback { |
|
if ($KeyInput -eq 27) { return $False } # exit with ESC-key |
|
|
|
If ($KeyInput -eq 32) { # select with space-key |
|
if ($Global:selected -contains $Selection) { # the index is already selected (in the list) |
|
$Global:selected.Remove($Selection) # remove the idx from the list |
|
} |
|
Else {$Global:selected.Add($Selection) } # remember the idx (NOT THE NAME, but the unique idx!) |
|
return $true # add and remove return false - we need to overwrite the return |
|
} |
|
} ` |
|
-Filter { |
|
if ($Global:selected -contains $Current) { |
|
$CellValue -Replace $CurrentValue,"$esc[4m$CurrentValue$esc[24m" # highlight selected with ansi sq for underline, BUT preserve cell value spaces |
|
} |
|
} | Out-Null # we only want the $global:Selected, we do not care about the default output |
|
Write-Host "Names: ", ( $Global:selected |% { $names[$_] } ) |
|
|
|
Allows to select multiple files with the space key, then gets their names. ESC will exit |
|
It uses an external var to keep track of selecting and unselecting, and uses the -Filter to modify the items |
|
|
|
.EXAMPLE |
|
$num = Create-Menu (1..10) ` |
|
-Title "Select a number:" ` |
|
-Filter { $CellValue -Replace $CurrentValue,"<$CurrentValue>" } ` |
|
-Clean ` |
|
-PassThrou |
|
Write-Host "You selected: ", $num |
|
|
|
Show a selection of numbers and reformat them. After selection, the menu will vanish and at its place show the selected number |
|
|
|
How? |
|
-Clean = Menu will vanish after selection |
|
-PassThrou = return the selected value, not index |
|
|
|
.NOTES |
|
Based on: https://community.spiceworks.com/scripts/show/4785-create-menu-2-0-arrow-key-driven-powershell-menu-for-scripts |
|
#> |
|
[CmdletBinding()] |
|
Param ( |
|
[Parameter(ValueFromPipeline=$True ,Mandatory=$True )][Alias("Options")][String[]]$MenuOptions, |
|
[Parameter(ValueFromPipeline=$False,Mandatory=$False)][Object]$Title = $Null, |
|
[Parameter(ValueFromPipeline=$False,Mandatory=$False)][Alias("Index")][int]$Selected = $Null, |
|
|
|
[Parameter(ValueFromPipeline=$False,Mandatory=$False)][Object]$Footer = $Null, |
|
[Parameter(ValueFromPipeline=$False,Mandatory=$False)][Alias("CB", "CallbackSelection")][ScriptBlock]$SelectionCallback = $Null, |
|
|
|
[Parameter(ValueFromPipeline=$False,Mandatory=$False)][String]$Columns = "Auto", |
|
[Parameter(ValueFromPipeline=$False,Mandatory=$False)][String]$MaximumColumnWidth = "Auto", |
|
[Parameter(ValueFromPipeline=$False,Mandatory=$False)][bool]$ShowCurrentSelection = $False, |
|
|
|
[Parameter(ValueFromPipeline=$False,Mandatory=$False)][switch]$PassThrou = $False, |
|
[Parameter(ValueFromPipeline=$False,Mandatory=$False)][switch]$ReturnObject = $False, |
|
|
|
[Parameter(ValueFromPipeline=$False,Mandatory=$False)][Alias("c")][ConsoleColor]$ForegroundColor = [Console]::ForegroundColor, |
|
[Parameter(ValueFromPipeline=$False,Mandatory=$False)][Alias("cs")][ConsoleColor]$ForegroundColorSelection = [ConsoleColor]::Black, |
|
[Parameter(ValueFromPipeline=$False,Mandatory=$False)][Alias("csb")][ConsoleColor]$BackgroundColorSelection = [ConsoleColor]::Cyan, |
|
[Parameter(ValueFromPipeline=$False,Mandatory=$False)][Alias("ct")][ConsoleColor]$ForegroundColorTitle = [ConsoleColor]::Yellow, |
|
[Parameter(ValueFromPipeline=$False,Mandatory=$False)][Alias("cf")][ConsoleColor]$ForegroundColorFooter = [ConsoleColor]::Green, |
|
|
|
[Parameter(ValueFromPipeline=$False,Mandatory=$False)][switch]$CleanHost = $False, |
|
[Parameter(ValueFromPipeline=$False,Mandatory=$False)][switch]$ClearHost = $False, |
|
|
|
[Parameter(ValueFromPipeline=$False,Mandatory=$False)][Alias('Filter')][Object]$FilterCallback = $Null |
|
) |
|
|
|
# in case items were pipelined |
|
$all = @($Input) |
|
If ($all) { |
|
$MenuOptions = [array]$all |
|
} |
|
|
|
$MenuOptionsInput = $MenuOptions.psobject.copy() # | ConvertTo-Json -depth 100 | ConvertFrom-Json |
|
$MaxValue = $MenuOptions.count-1 |
|
If ($Selected) { $Selection = $Selected } else { $Selection = 0 } |
|
$EnterPressed = $False |
|
$WindowTitleBackup = $Host.UI.RawUI.WindowTitle |
|
$RowQty = 0 |
|
|
|
$outputBuffer = "" # contains output |
|
|
|
If ($Columns -eq "Auto") { |
|
If ($MaximumColumnWidth -eq "Auto") { |
|
$MaximumColumnWidth = 20 |
|
} |
|
|
|
$WindowWidth = $Host.UI.RawUI.BufferSize.Width |
|
$Columns = [Math]::Floor($WindowWidth / ([int]$MaximumColumnWidth +2)) |
|
} |
|
Else { |
|
If ($MaximumColumnWidth -eq "Auto") { |
|
$MaximumColumnWidth = [Math]::Floor(($Host.UI.RawUI.BufferSize.Width - [int]$Columns) / [int]$Columns) |
|
} |
|
} |
|
|
|
$MenuListing = @() |
|
Function New-MenuListing($MenuOptions) { |
|
If ([int]$Columns -gt $MenuOptions.count) { |
|
$Columns = $MenuOptions.count |
|
} |
|
|
|
$RowQty = ([Math]::Ceiling(($MaxValue +1) / [int]$Columns)) |
|
|
|
# This loop is used to format the menu listing into a |
|
# two-dimensional array, where each row is a column in the |
|
# final menu listing. The outer loop iterates over the columns |
|
# while the inner loop iterates over the rows of the column. |
|
# The inner loop fetches the menu options for each column and |
|
# then formats them to fit within the maximum column width. |
|
# The formatted menu options are stored in the $MenuListing array. |
|
For ($i=0; $i -lt $Columns; $i++) { |
|
|
|
$ScratchArray = @() |
|
|
|
For ($j=($RowQty*$i); $j -lt ($RowQty*($i+1)); $j++) { |
|
|
|
$ScratchArray += $MenuOptions[$j] |
|
} |
|
|
|
$ColWidth = ($ScratchArray |Measure-Object -Maximum -Property length).Maximum |
|
|
|
If ($ColWidth -gt [int]$MaximumColumnWidth) { |
|
$ColWidth = [int]$MaximumColumnWidth-1 |
|
} |
|
|
|
For ($j=0; $j -lt $ScratchArray.count; $j++) { |
|
|
|
If (($ScratchArray[$j]).length -gt $([int]$MaximumColumnWidth -2)) { |
|
$ScratchArray[$j] = $($ScratchArray[$j]).Substring(0,$([int]$MaximumColumnWidth-2)) |
|
$ScratchArray[$j] = "$($ScratchArray[$j])…" |
|
} |
|
Else { |
|
For ($k=$ScratchArray[$j].length; $k -lt $ColWidth; $k++) { |
|
$ScratchArray[$j] = "$($ScratchArray[$j]) " |
|
} |
|
} |
|
|
|
$ScratchArray[$j] = " $($ScratchArray[$j]) " |
|
} |
|
$MenuListing += $ScratchArray |
|
} |
|
return $MenuListing, $Columns, $RowQty |
|
} |
|
|
|
Function New-TextBlock ($Block, $BlockName, $ForegroundColor = -1, $Selection, $MenuOptions, $MenuOptionsInput, $esc, $CL) { |
|
$retBuffer = "" |
|
if ($Block) { |
|
if ($Block -is [String]) { |
|
$retBuffer += ConvertTo-CreateMenuAnsiColorString $Block -ForegroundColor $ForegroundColor |
|
} |
|
else { |
|
$retBuffer += Invoke-Command -ScriptBlock { param($Selection, $SelectionValue, $MenuOptions, $MenuOptionsInput, $esc, $CL) # supply usable variables |
|
|
|
# in module fix |
|
try { $sbStr = Get-Variable -Name "Block" -Scope 1 -ValueOnly ; $Block = [scriptblock]::Create($sbStr) } |
|
catch { } |
|
|
|
Invoke-Command -ScriptBlock $Block *>&1 -OutVariable Lines | Out-Null |
|
$retBuffer += ConvertTo-CreateMenuAnsiColorString $Lines -ForegroundColor $ForegroundColor # manual color |
|
|
|
return $retBuffer |
|
} -ArgumentList $Selection, $MenuOptions[$Selection], $MenuOptions, $MenuOptionsInput, $esc, $CL |
|
} |
|
} |
|
return $retBuffer | ForEach-Object { "$CL$_" } #! ALWAYS clean the line before using |
|
} |
|
|
|
Function CleanHost { |
|
$Host.UI.RawUI.CursorPosition = @{x=0; y=$Host.UI.RawUI.CursorPosition.Y - $bufferHeight} |
|
Write-Host ("$CL`n" *( $bufferHeight)).Trim() |
|
$Host.UI.RawUI.CursorPosition = @{x=0; y=$Host.UI.RawUI.CursorPosition.Y - $bufferHeight} |
|
} |
|
|
|
|
|
$MenuListing, $Columns, $RowQty = New-MenuListing $MenuOptions |
|
|
|
[Console]::CursorVisible = $False |
|
|
|
|
|
$bufferHeight = 0 |
|
$bufferHeightBackup = 0 |
|
|
|
$esc = [char]27 |
|
$CL = "$esc[0;0m$esc[0J" |
|
|
|
$topBufferHeight = -1 |
|
|
|
|
|
if ($ClearHost) { |
|
[Console]::Clear() |
|
} |
|
|
|
While ($True) { |
|
$outputBuffer = "" |
|
$bufferHeightBackup = $bufferHeight |
|
|
|
If ($ShowCurrentSelection) { |
|
$Host.UI.RawUI.WindowTitle = "CURRENT SELECTION: $($MenuOptions[$Selection])" |
|
} |
|
|
|
# set cursor back to beginning of output (top of screen has to be processed at least once) |
|
if ($topBufferHeight -ge 0 -and ($DebugPreference -ne 'Continue') ) { |
|
$Host.UI.RawUI.CursorPosition = @{x=0; y=$Host.UI.RawUI.CursorPosition.Y - $topBufferHeight} |
|
} |
|
|
|
|
|
# generate output |
|
|
|
$outputBuffer += New-TextBlock $Title "title-block" $ForegroundColorTitle $Selection $MenuOptions $MenuOptionsInput $esc $CL |
|
|
|
# output selections |
|
For ($i=0; $i -lt $RowQty; $i++) { |
|
|
|
For ($j=0; $j -le (($Columns-1)*$RowQty);$j+=$RowQty) { |
|
|
|
$value = $MenuListing[$i+$j] |
|
|
|
if ($null -ne $FilterCallback) { |
|
$MenuOptions,$Selection,$CellValue = Invoke-Command -ScriptBlock { param($CellValue, $Current, $CurrentValue, $Selection, $SelectionValue, $MenuOptions, $MenuOptionsInput, $esc, $CL, $Row, $Column) |
|
|
|
# in module fix |
|
try { $sbStr = Get-Variable -Name "FilterCallback" -Scope 1 -ValueOnly ; $FilterCallback = [scriptblock]::Create($sbStr) } |
|
catch { } |
|
|
|
$CellValue = . $FilterCallback |
|
|
|
Return $MenuOptions, $Selection, $CellValue |
|
} -ArgumentList $value, ($i+$j), $MenuOptions[$i+$j] , $Selection, $MenuOptions[$Selection], $MenuOptions, $MenuOptionsInput, $esc, $CL, $i, $j |
|
|
|
$value = if ($CellValue) { $CellValue } else { $value } |
|
} |
|
|
|
# is end of line |
|
If ($j -eq (($Columns-1)*$RowQty)) { |
|
If (($i+$j) -eq $Selection){ |
|
$outputBuffer += ConvertTo-CreateMenuAnsiColorString "$($value)$CL`n" -BackgroundColor $BackgroundColorSelection -ForegroundColor $ForegroundColorSelection |
|
} Else { |
|
$outputBuffer += ConvertTo-CreateMenuAnsiColorString "$($value)`n" -ForegroundColor $ForegroundColor |
|
} |
|
# is in row |
|
} Else { |
|
If (($i+$j) -eq $Selection) { |
|
$outputBuffer += ConvertTo-CreateMenuAnsiColorString "$($value)" -BackgroundColor $BackgroundColorSelection -ForegroundColor $ForegroundColorSelection |
|
} Else { |
|
$outputBuffer += ConvertTo-CreateMenuAnsiColorString "$($value)" -ForegroundColor $ForegroundColor |
|
} |
|
} |
|
|
|
} |
|
} |
|
|
|
$outputBuffer += New-TextBlock $Footer "footer-block" $ForegroundColorFooter $Selection $MenuOptions $MenuOptionsInput $esc $CL |
|
|
|
|
|
$outputBuffer += " " # prevents line jumping |
|
|
|
# do positioning |
|
|
|
$bufferHeight = ($outputBuffer | Measure-Object -Line).lines |
|
$currentBufferHeight = $Host.UI.RawUI.BufferSize.Height |
|
|
|
If ($bufferHeight -gt $currentBufferHeight) { |
|
throw "Menu too tall to fit in buffer. Increase buffer height. (Window height)" |
|
} |
|
|
|
Write-Host $outputBuffer |
|
|
|
$topBufferHeight = $bufferHeight # back to start line |
|
if ($bufferHeight -lt $bufferHeightBackup) { # rows changed inbetween |
|
Write-Host ("$CL`n" *( $bufferHeightBackup - $bufferHeight)).Trim() |
|
$topBufferHeight = $bufferHeightBackup |
|
} |
|
|
|
# $Host.UI.RawUI.CursorPosition = @{x=0; y=$Host.UI.RawUI.CursorPosition.Y - $topBufferHeight} |
|
|
|
|
|
if ($EnterPressed) { |
|
If ($ClearHost) { |
|
[Console]::Clear() |
|
} |
|
|
|
Break |
|
} |
|
|
|
$KeyInput = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown").virtualkeycode |
|
|
|
Switch ($KeyInput) { |
|
13 { #Enter |
|
# ignore empty fields |
|
If ($MenuOptions[$Selection]) { |
|
$EnterPressed = $True |
|
|
|
# set title to before menu |
|
If ($ShowCurrentSelection) { |
|
$Host.UI.RawUI.WindowTitle = $WindowTitleBackup |
|
} |
|
|
|
If ($ClearHost) { |
|
[Console]::Clear() |
|
} |
|
|
|
if ($CleanHost) { |
|
CleanHost |
|
} |
|
|
|
$returnContent = $null |
|
|
|
If ($ReturnObject) { |
|
|
|
$items = @() |
|
|
|
For ($i=0; $i -lt $MenuOptions.length; $i++) { |
|
$items += @{ |
|
"Name" = $MenuOptions[$i]; |
|
"Index" = $i; |
|
"Input" = $MenuOptionsInput[$i]; |
|
} |
|
} |
|
|
|
$returnContent = @{ |
|
"Selection" = $Selection; |
|
"SelectionValue" = $MenuOptions[$Selection]; |
|
"MenuOptions" = $MenuOptions; |
|
"MenuOptionsInput" = $MenuOptionsInput; |
|
"Items" = $items; |
|
} |
|
} |
|
ElseIf ($PassThrou) { |
|
$returnContent = $MenuOptions[$Selection] |
|
} |
|
Else { |
|
$returnContent = $Selection |
|
} |
|
# $Host.UI.RawUI.CursorPosition = @{x=0; y=$Host.UI.RawUI.CursorPosition.Y + $BufferHeight} |
|
[Console]::CursorVisible = $true |
|
|
|
Return $returnContent |
|
} |
|
|
|
Break |
|
} |
|
|
|
37 { #Left |
|
If ($Selection -ge $RowQty){ |
|
$Selection -= $RowQty |
|
} Else { |
|
$Selection += ($Columns-1)*$RowQty |
|
} |
|
Break |
|
} |
|
|
|
38 { #Up |
|
If ((($Selection+$RowQty)%$RowQty) -eq 0) { |
|
# If the selection is at the start of a row, move to the last item in the row |
|
$Selection += $RowQty - 1 |
|
} Else { |
|
$Selection -= 1 |
|
} |
|
Break |
|
} |
|
|
|
39{ #Right |
|
# If the selection is at the end of a row, move to the previous row |
|
# by subtracting the number of columns minus one multiplied by the row quantity |
|
# Otherwise, move to the next item in the row |
|
If ([Math]::Ceiling($Selection/$RowQty) -eq $Columns -or ($Selection/$RowQty)+1 -eq $Columns){ |
|
$Selection -= ($Columns-1)*$RowQty |
|
} Else { |
|
$Selection += $RowQty |
|
} |
|
Break |
|
} |
|
|
|
40 { #Down |
|
If ((($Selection+1)%$RowQty) -eq 0 -or $Selection -eq $MaxValue){ |
|
$Selection = ([Math]::Floor(($Selection)/$RowQty))*$RowQty |
|
} Else { |
|
$Selection += 1 |
|
} |
|
Break |
|
} |
|
|
|
Default { |
|
} |
|
} |
|
|
|
if ($SelectionCallback -ne $Null) { |
|
# return new MenuOptions, if you want |
|
$ret,$sel,$res = Invoke-Command -ScriptBlock { param($Selection, $SelectionValue, $MenuOptions, $MenuOptionsInput, $KeyInput, $esc, $CL) |
|
|
|
# in module fix, replace wiht localized script block |
|
try { $sbStr = Get-Variable -Name "SelectionCallback" -Scope 1 -ValueOnly ; $SelectionCallback = [scriptblock]::Create($sbStr) } |
|
catch { } |
|
|
|
$res = . $SelectionCallback |
|
|
|
Return $MenuOptions, $Selection, $res |
|
} -ArgumentList $Selection, $MenuOptions[$Selection], $MenuOptions, $MenuOptionsInput, $KeyInput, $esc, $CL |
|
|
|
If ($sel -is [Int]) { |
|
$Selection = $sel |
|
} |
|
|
|
If ($res -eq $False) { |
|
$EnterPressed = $True |
|
|
|
# set title to before menu |
|
If ($ShowCurrentSelection) { |
|
$Host.UI.RawUI.WindowTitle = $WindowTitleBackup |
|
} |
|
} |
|
|
|
ElseIf ($ret -is [Array]) { |
|
$MenuOptions = $ret |
|
$MenuListing, $Columns, $RowQty = New-MenuListing $MenuOptions |
|
} |
|
} |
|
|
|
} |
|
} |
|
Function ConvertTo-CreateMenuAnsiColorString { |
|
[CmdletBinding()] |
|
Param ( |
|
[Parameter(Mandatory=$true, Position=0)] |
|
[string]$InputString, |
|
[Parameter(Mandatory=$false)] |
|
[string]$ForegroundColor, |
|
[Parameter(Mandatory=$false)] |
|
[string]$BackgroundColor, |
|
[Parameter(Mandatory=$false)] |
|
[string[]]$Styles |
|
) |
|
|
|
$esc = [char]27 # using char 27 instead of string "`e" because not using `e makeis it PS 5.1 compatible |
|
|
|
$ansiColors = @{ |
|
# Powershell defined ones, 0-15 |
|
Black = 0; DarkBlue = 4; DarkGreen = 2; DarkCyan = 6; DarkRed = 1; DarkMagenta = 5; DarkYellow = 3; Gray = 7; DarkGray = 8; Blue = 12; Green = 10; Cyan = 14; Red = 9; Magenta = 13; Yellow = 11; White = 15; |
|
} |
|
$fgColor = $ansiColors[$ForegroundColor] |
|
$bgColor = $ansiColors[$BackgroundColor] |
|
|
|
$ansiStyles = @{ Normal = 0; Bold = 1; Dim = 2; Italic = 3; Underline = 4; Blink = 5; RapidBlink = 6; Reverse = 7; Hidden = 8; } |
|
if ($Styles) { |
|
$styleSequence = $Styles | ForEach-Object { "$esc[{0}m" -f $ansiStyles[$_] } |
|
} |
|
|
|
if ("$fgColor" -and "$bgColor") { |
|
return "$styleSequence$esc[38;5;{0};48;5;{1}m{2}$esc[0m" -f $fgColor, $bgColor, $InputString |
|
} elseif ("$fgColor") { |
|
return "$styleSequence$esc[38;5;{0}m{1}$esc[0m" -f $fgColor, $InputString |
|
} elseif ("$bgColor") { |
|
return "$styleSequence$esc[48;5;{0}m{1}$esc[0m" -f $bgColor, $InputString |
|
} elseif ($Styles) { |
|
return "$styleSequence$InputString$esc[0m" |
|
} else { |
|
return $InputString |
|
} |
|
} |
|
|
|
|
|
Main |
throws a bunch of errors on my side and leaves me with a blank .ps1