Files
win-setup-menu/setup-menu.ps1
T
2026-07-04 13:37:02 +08:00

2259 lines
58 KiB
PowerShell

# install-menu.ps1
$ErrorActionPreference = "Stop"
$SoftwareGroups = [ordered]@{
"System Tools" = @(
@{ Type = "Winget"; Name = "7-Zip"; Id = "7zip.7zip" }
@{ Type = "Winget"; Name = "Git"; Id = "Git.Git" }
@{ Type = "Winget"; Name = "PowerShell 7"; Id = "Microsoft.PowerShell" }
@{ Type = "Winget"; Name = "Windows Terminal"; Id = "Microsoft.WindowsTerminal" }
@{ Type = "Winget"; Name = "PowerToys"; Id = "Microsoft.PowerToys" }
@{ Type = "Winget"; Name = "Everything"; Id = "voidtools.Everything" }
@{ Type = "Winget"; Name = "VC++ Runtime"; Id = "Microsoft.VCRedist.2015+.x64" }
@{ Type = "Winget"; Name = "CrystalDiskInfo Aoi Edition"; Id = "CrystalDewWorld.CrystalDiskInfo.AoiEdition" }
@{ Type = "Winget"; Name = "CPUID CPU-Z"; Id = "CPUID.CPU-Z" }
@{ Type = "Winget"; Name = "HWiNFO"; Id = "REALiX.HWiNFO" }
)
"Apps" = @(
@{ Type = "Winget"; Name = "Google Chrome"; Id = "Google.Chrome" }
@{ Type = "Winget"; Name = "WeChat"; Id = "Tencent.WeChat" }
@{ Type = "Winget"; Name = "QQ"; Id = "Tencent.QQ" }
@{ Type = "Winget"; Name = "Telegram"; Id = "Telegram.TelegramDesktop" }
@{ Type = "Winget"; Name = "VLC"; Id = "VideoLAN.VLC" }
@{ Type = "Winget"; Name = "Obsidian"; Id = "Obsidian.Obsidian" }
@{ Type = "Winget"; Name = "Office Tool Plus"; Id = "Yerong.OfficeToolPlus" }
@{ Type = "Winget"; Name = "Neovide"; Id = "Neovide.Neovide" }
@{ Type = "Winget"; Name = "Syncthing"; Id = "Syncthing.Syncthing"; ProcessName = "syncthing"; AutostartName = "Syncthing" }
@{ Type = "Winget"; Name = "Steam"; Id = "Valve.Steam" }
)
"Command Line Tools" = @(
@{ Type = "Winget"; Name = "Yazi"; Id = "sxyazi.yazi" }
@{ Type = "Winget"; Name = "Neovim"; Id = "Neovim.Neovim" }
@{ Type = "Winget"; Name = "eza"; Id = "eza-community.eza" }
@{ Type = "Winget"; Name = "zoxide"; Id = "ajeetdsouza.zoxide" }
@{ Type = "Winget"; Name = "fzf"; Id = "junegunn.fzf" }
@{ Type = "Winget"; Name = "ripgrep"; Id = "BurntSushi.ripgrep.MSVC" }
@{ Type = "Winget"; Name = "fd"; Id = "sharkdp.fd" }
@{ Type = "Winget"; Name = "bat"; Id = "sharkdp.bat" }
@{ Type = "Winget"; Name = "jq"; Id = "jqlang.jq" }
@{ Type = "Winget"; Name = "curl"; Id = "cURL.cURL" }
@{ Type = "Winget"; Name = "wget"; Id = "GNU.Wget2" }
@{ Type = "Winget"; Name = "starship"; Id = "Starship.Starship" }
@{ Type = "Winget"; Name = "fastfetch"; Id = "Fastfetch-cli.Fastfetch" }
@{ Type = "Winget"; Name = "lazygit"; Id = "JesseDuffield.lazygit" }
@{ Type = "Winget"; Name = "git-delta"; Id = "dandavison.delta" }
@{ Type = "Winget"; Name = "bottom"; Id = "Clement.bottom" }
@{ Type = "Winget"; Name = "FFmpeg"; Id = "Gyan.FFmpeg" }
@{ Type = "Winget"; Name = "ImageMagick"; Id = "ImageMagick.ImageMagick" }
@{ Type = "Winget"; Name = "Poppler"; Id = "oschwartz10612.Poppler" }
)
"Dev Tools" = @(
@{ Type = "Winget"; Name = "Visual Studio Code"; Id = "Microsoft.VisualStudioCode" }
@{ Type = "Winget"; Name = "GitHub CLI"; Id = "GitHub.cli" }
@{ Type = "Winget"; Name = "Python 3.13"; Id = "Python.Python.3.13" }
@{ Type = "Winget"; Name = "Node.js LTS"; Id = "OpenJS.NodeJS.LTS" }
@{ Type = "Winget"; Name = "uv"; Id = "astral-sh.uv" }
@{ Type = "Winget"; Name = "Docker Desktop"; Id = "Docker.DockerDesktop" }
)
"Fonts" = @(
@{ Type = "Winget"; Name = "JetBrains Mono Nerd Font"; Id = "DEVCOM.JetBrainsMonoNerdFont" }
@{ Type = "Winget"; Name = "Cascadia Code Nerd Font"; Id = "DEVCOM.CascadiaCodeNerdFont" }
@{ Type = "Winget"; Name = "Cascadia Mono Nerd Font"; Id = "DEVCOM.CascadiaMonoNerdFont" }
@{ Type = "Winget"; Name = "FiraCode Nerd Font"; Id = "DEVCOM.FiraCodeNerdFont" }
@{ Type = "Winget"; Name = "Hack Nerd Font"; Id = "DEVCOM.HackNerdFont" }
)
"WSL / Linux" = @(
@{ Type = "WslBase"; Name = "WSL base only"; Exe = "wsl"; Args = @("--install", "--no-distribution") }
@{ Type = "WslDistro"; Name = "Ubuntu"; Exe = "wsl"; Args = @("--install", "-d", "Ubuntu") }
@{ Type = "WslDistro"; Name = "Debian"; Exe = "wsl"; Args = @("--install", "-d", "Debian") }
@{ Type = "WslDistro"; Name = "Kali Linux"; Exe = "wsl"; Args = @("--install", "-d", "kali-linux") }
@{ Type = "WslDistro"; Name = "Arch Linux"; Exe = "wsl"; Args = @("--install", "-d", "archlinux") }
)
"Autostart" = @(
@{ Type = "Autostart"; Name = "Syncthing"; Command = "syncthing.exe"; Args = @("--no-console", "--no-browser"); ShortcutName = "Syncthing.lnk"; Url = "http://localhost:8384" }
)
"Package Downloads" = @(
# Put direct download links here. Files are downloaded to Downloads\packages by default.
# Example:
# @{ Type = "Download"; Name = "Example ZIP Package"; Url = "https://example.com/package.zip"; FileName = "package.zip" }
@{ Type = "Download"; Name = "HEU_KMS_Activator"; Url = "https://github.com/zbezj/HEU_KMS_Activator/releases/download/63.4.0/HEU_KMS_Activator_v63.4.0.rar"; FileName = "HEU_KMS_Activator_v63.4.0.zip" }
)
"Maintenance" = @(
@{ Type = "Command"; Name = "Update winget source"; Exe = "winget"; Args = @("source", "update") }
@{ Type = "Command"; Name = "Upgrade all winget packages"; Exe = "winget"; Args = @("upgrade", "--all", "--accept-source-agreements", "--accept-package-agreements") }
@{ Type = "Command"; Name = "List installed packages"; Exe = "winget"; Args = @("list") }
@{ Type = "Command"; Name = "Export installed packages"; Exe = "winget"; Args = @("export", "-o", "winget-export.json", "--accept-source-agreements") }
@{ Type = "Command"; Name = "WSL status"; Exe = "wsl"; Args = @("--status") }
@{ Type = "Command"; Name = "List online WSL distros"; Exe = "wsl"; Args = @("--list", "--online") }
)
}
function Write-Line {
Write-Host "----------------------------------------" -ForegroundColor DarkGray
}
function Write-Title {
param (
[string]$Text
)
Write-Line
Write-Host " $Text" -ForegroundColor Cyan
Write-Line
}
function Write-MenuItem {
param (
[int]$Number,
[string]$Name,
[string]$Extra = ""
)
Write-Host ("{0,2}. " -f $Number) -NoNewline -ForegroundColor Yellow
Write-Host $Name -NoNewline -ForegroundColor White
if ($Extra -ne "") {
Write-Host " $Extra" -ForegroundColor DarkGray
}
else {
Write-Host ""
}
}
function Write-Option {
param (
[string]$Key,
[string]$Text
)
Write-Host " $Key. " -NoNewline -ForegroundColor Yellow
Write-Host $Text -ForegroundColor White
}
function Write-Hint {
param (
[string]$Text
)
Write-Host $Text -ForegroundColor DarkGray
}
function Write-Info {
param (
[string]$Text
)
Write-Host $Text -ForegroundColor Cyan
}
function Write-Success {
param (
[string]$Text
)
Write-Host $Text -ForegroundColor Green
}
function Write-Warn {
param (
[string]$Text
)
Write-Host $Text -ForegroundColor Yellow
}
function Write-ErrorText {
param (
[string]$Text
)
Write-Host $Text -ForegroundColor Red
}
function Write-Kv {
param (
[string]$Key,
[string]$Value
)
Write-Host ("{0,-8}: " -f $Key) -NoNewline -ForegroundColor DarkGray
Write-Host $Value -ForegroundColor White
}
function Wait-User {
Write-Host ""
Read-Host "Press Enter to continue"
}
function Test-Winget {
if (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
Write-ErrorText "winget not found. Please install App Installer first."
return $false
}
return $true
}
function Test-WslCommand {
if (-not (Get-Command wsl -ErrorAction SilentlyContinue)) {
Write-ErrorText "wsl command not found."
Write-Warn "Please make sure your Windows version supports WSL."
return $false
}
return $true
}
function Test-CommandExists {
param (
[string]$CommandName
)
if (-not (Get-Command $CommandName -ErrorAction SilentlyContinue)) {
Write-ErrorText "$CommandName not found."
return $false
}
return $true
}
function Test-WingetInstalled {
param (
[string]$Id
)
if (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
return $false
}
$output = winget list --id $Id -e --accept-source-agreements 2>$null | Out-String
if ($output -match [regex]::Escape($Id)) {
return $true
}
return $false
}
function Test-WslReady {
if (-not (Get-Command wsl -ErrorAction SilentlyContinue)) {
return $false
}
wsl --status *> $null
if ($LASTEXITCODE -eq 0) {
return $true
}
return $false
}
function Get-WslDistroName {
param (
[hashtable]$App
)
if (-not $App.Args) {
return ""
}
$argsText = @($App.Args)
for ($i = 0; $i -lt $argsText.Count; $i++) {
if ($argsText[$i] -eq "-d" -and ($i + 1) -lt $argsText.Count) {
return [string]$argsText[$i + 1]
}
}
if ($argsText.Count -ge 2 -and $argsText[0] -eq "--install") {
return [string]$argsText[1]
}
return ""
}
function Test-WslDistroInstalled {
param (
[hashtable]$App
)
if (-not (Test-WslReady)) {
return $false
}
$distroName = Get-WslDistroName -App $App
if ($distroName -eq "") {
return $false
}
$distros = wsl --list --quiet 2>$null
foreach ($distro in $distros) {
if ($distro.Trim() -ieq $distroName) {
return $true
}
}
return $false
}
function Test-IsAdmin {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = [Security.Principal.WindowsPrincipal]$identity
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
function Restart-ScriptAsAdmin {
$scriptPath = $PSCommandPath
if (-not $scriptPath) {
Write-Warn "Cannot find current script path."
Write-Warn "Please run PowerShell as Administrator manually."
Wait-User
return
}
Write-Warn "Administrator permission is required."
Write-Hint "A new Administrator PowerShell window will be opened."
Write-Host ""
$confirm = Read-Host "Restart this script as Administrator? Y/N"
if ($confirm -notmatch "^[Yy]$") {
Write-Warn "Cancelled."
Wait-User
return
}
$argumentList = "-ExecutionPolicy Bypass -File `"$scriptPath`""
Start-Process powershell -Verb RunAs -ArgumentList $argumentList
exit
}
function Get-StartupFolder {
return [Environment]::GetFolderPath("Startup")
}
function Get-AutostartShortcutPath {
param (
[hashtable]$App
)
$startupFolder = Get-StartupFolder
$shortcutName = [string]$App.ShortcutName
if ([string]::IsNullOrWhiteSpace($shortcutName)) {
$shortcutName = "$($App.Name).lnk"
}
return (Join-Path $startupFolder $shortcutName)
}
function Get-AutostartArgumentsText {
param (
[hashtable]$App
)
if (-not $App.Args) {
return ""
}
return (@($App.Args) -join " ")
}
function Get-AutostartLauncherFolder {
$folder = Join-Path $env:LOCALAPPDATA "InstallMenu\Autostart"
return $folder
}
function Get-AutostartLauncherPath {
param (
[hashtable]$App
)
$name = [string]$App.Name
if ([string]::IsNullOrWhiteSpace($name)) {
$name = "AutostartApp"
}
$safeName = ($name -replace '[^a-zA-Z0-9._-]', '_')
return (Join-Path (Get-AutostartLauncherFolder) "$safeName-hidden.vbs")
}
function Get-AutostartCommandLine {
param (
[string]$ExePath,
[string]$ArgumentsText
)
$commandLine = "`"$ExePath`""
if (-not [string]::IsNullOrWhiteSpace($ArgumentsText)) {
$commandLine = "$commandLine $ArgumentsText"
}
return $commandLine
}
function New-AutostartHiddenLauncher {
param (
[hashtable]$App,
[string]$ExePath,
[string]$ArgumentsText
)
$launcherFolder = Get-AutostartLauncherFolder
$launcherPath = Get-AutostartLauncherPath -App $App
if (-not (Test-Path $launcherFolder)) {
New-Item -ItemType Directory -Path $launcherFolder -Force | Out-Null
}
$commandLine = Get-AutostartCommandLine -ExePath $ExePath -ArgumentsText $ArgumentsText
$vbsCommandLine = $commandLine.Replace('"', '""')
$content = @"
Set WshShell = CreateObject("WScript.Shell")
WshShell.Run "$vbsCommandLine", 0, False
"@
Set-Content -Path $launcherPath -Value $content -Encoding Unicode
return $launcherPath
}
function Start-HiddenProgram {
param (
[string]$ExePath,
[string]$ArgumentsText
)
$commandLine = Get-AutostartCommandLine -ExePath $ExePath -ArgumentsText $ArgumentsText
$wsh = New-Object -ComObject WScript.Shell
$wsh.Run($commandLine, 0, $false) | Out-Null
}
function Get-AutostartCommandInfo {
param (
[hashtable]$App
)
$commandName = [string]$App.Command
if ([string]::IsNullOrWhiteSpace($commandName)) {
return $null
}
return (Get-Command $commandName -ErrorAction SilentlyContinue)
}
function Get-AutostartExePath {
param (
[hashtable]$App
)
$commandInfo = Get-AutostartCommandInfo -App $App
if ($null -eq $commandInfo) {
return ""
}
if ($commandInfo.Source) {
return [string]$commandInfo.Source
}
if ($commandInfo.Path) {
return [string]$commandInfo.Path
}
return ""
}
function Get-AutostartProcessName {
param (
[hashtable]$App
)
if ($App.ProcessName) {
return [string]$App.ProcessName
}
$commandName = [string]$App.Command
if ([string]::IsNullOrWhiteSpace($commandName)) {
return ""
}
return [System.IO.Path]::GetFileNameWithoutExtension($commandName)
}
function Test-AutostartEnabled {
param (
[hashtable]$App
)
$shortcutPath = Get-AutostartShortcutPath -App $App
return (Test-Path $shortcutPath)
}
function Test-AutostartProcessRunning {
param (
[hashtable]$App
)
$processName = Get-AutostartProcessName -App $App
if ([string]::IsNullOrWhiteSpace($processName)) {
return $false
}
$process = Get-Process -Name $processName -ErrorAction SilentlyContinue
return ($null -ne $process)
}
function Stop-AutostartAppNow {
param (
[hashtable]$App,
[switch]$NoPause,
[switch]$Silent
)
$processName = Get-AutostartProcessName -App $App
if ([string]::IsNullOrWhiteSpace($processName)) {
if (-not $Silent) {
Write-Warn "No process name configured for $($App.Name)."
}
if (-not $NoPause) {
Wait-User
}
return $false
}
$processes = Get-Process -Name $processName -ErrorAction SilentlyContinue
if ($null -eq $processes) {
if (-not $Silent) {
Write-Warn "$($App.Name) is not running."
}
if (-not $NoPause) {
Wait-User
}
return $true
}
try {
$processes | Stop-Process -Force
if (-not $Silent) {
Write-Success "Stopped: $($App.Name)"
}
if (-not $NoPause) {
Wait-User
}
return $true
}
catch {
if (-not $Silent) {
Write-ErrorText "Failed to stop $($App.Name): $($_.Exception.Message)"
}
if (-not $NoPause) {
Wait-User
}
return $false
}
}
function Enable-AutostartShortcut {
param (
[hashtable]$App,
[switch]$NoPause
)
$exePath = Get-AutostartExePath -App $App
if ([string]::IsNullOrWhiteSpace($exePath)) {
Write-ErrorText "$($App.Name) command not found: $($App.Command)"
Write-Warn "Please install $($App.Name) first, then enable autostart."
if (-not $NoPause) {
Wait-User
}
return $false
}
$startupFolder = Get-StartupFolder
$shortcutPath = Get-AutostartShortcutPath -App $App
$argumentsText = Get-AutostartArgumentsText -App $App
if (-not (Test-Path $startupFolder)) {
New-Item -ItemType Directory -Path $startupFolder -Force | Out-Null
}
$launcherPath = New-AutostartHiddenLauncher -App $App -ExePath $exePath -ArgumentsText $argumentsText
$wscriptPath = Join-Path $env:WINDIR "System32\wscript.exe"
$wsh = New-Object -ComObject WScript.Shell
$shortcut = $wsh.CreateShortcut($shortcutPath)
$shortcut.TargetPath = $wscriptPath
$shortcut.Arguments = "//B //Nologo `"$launcherPath`""
$shortcut.WorkingDirectory = Split-Path $launcherPath
$shortcut.Description = "Start $($App.Name) hidden at Windows user logon"
$shortcut.WindowStyle = 7
$shortcut.Save()
Write-Success "Autostart enabled: $($App.Name)"
Write-Hint "Shortcut: $shortcutPath"
Write-Hint "Launcher: $launcherPath"
Write-Hint "Command : $exePath $argumentsText"
if (-not $NoPause) {
Wait-User
}
return $true
}
function Disable-AutostartShortcut {
param (
[hashtable]$App,
[switch]$NoPause
)
$shortcutPath = Get-AutostartShortcutPath -App $App
$launcherPath = Get-AutostartLauncherPath -App $App
$removed = $false
if (Test-Path $shortcutPath) {
Remove-Item $shortcutPath -Force
Write-Success "Removed shortcut: $shortcutPath"
$removed = $true
}
if (Test-Path $launcherPath) {
Remove-Item $launcherPath -Force
Write-Success "Removed launcher: $launcherPath"
$removed = $true
}
if ($removed) {
Write-Success "Autostart disabled: $($App.Name)"
}
else {
Write-Warn "Autostart is not enabled: $($App.Name)"
Write-Hint "Shortcut not found: $shortcutPath"
}
if (-not $NoPause) {
Wait-User
}
}
function Start-AutostartAppNow {
param (
[hashtable]$App,
[switch]$NoPause
)
$exePath = Get-AutostartExePath -App $App
if ([string]::IsNullOrWhiteSpace($exePath)) {
Write-ErrorText "$($App.Name) command not found: $($App.Command)"
Write-Warn "Please install $($App.Name) first."
if (-not $NoPause) {
Wait-User
}
return
}
if (Test-AutostartProcessRunning -App $App) {
Write-Warn "$($App.Name) is already running."
if (-not $NoPause) {
Wait-User
}
return
}
$argumentsText = Get-AutostartArgumentsText -App $App
Start-HiddenProgram -ExePath $exePath -ArgumentsText $argumentsText
Write-Success "Started hidden: $($App.Name)"
if (-not $NoPause) {
Wait-User
}
}
function Open-AutostartAppUrl {
param (
[hashtable]$App,
[switch]$NoPause
)
$url = [string]$App.Url
if ([string]::IsNullOrWhiteSpace($url)) {
Write-Warn "No URL configured for $($App.Name)."
}
else {
Start-Process $url
Write-Success "Opened: $url"
}
if (-not $NoPause) {
Wait-User
}
}
function Show-AutostartInfo {
param (
[hashtable]$App
)
Clear-Host
Write-Title "Autostart Info"
$shortcutPath = Get-AutostartShortcutPath -App $App
$exePath = Get-AutostartExePath -App $App
$argumentsText = Get-AutostartArgumentsText -App $App
$enabledText = if (Test-AutostartEnabled -App $App) { "Enabled" } else { "Disabled" }
$runningText = if (Test-AutostartProcessRunning -App $App) { "Running" } else { "Not running" }
Write-Kv -Key "Name" -Value $App.Name
Write-Kv -Key "Type" -Value "Startup folder shortcut + hidden launcher"
Write-Kv -Key "Status" -Value $enabledText
Write-Kv -Key "Running" -Value $runningText
Write-Kv -Key "Startup" -Value (Get-StartupFolder)
Write-Kv -Key "Shortcut" -Value $shortcutPath
Write-Kv -Key "Launcher" -Value (Get-AutostartLauncherPath -App $App)
Write-Kv -Key "Command" -Value "$($App.Command) $argumentsText"
if ([string]::IsNullOrWhiteSpace($exePath)) {
Write-Kv -Key "Exe" -Value "Not found"
}
else {
Write-Kv -Key "Exe" -Value $exePath
}
if ($App.Url) {
Write-Kv -Key "Web UI" -Value ([string]$App.Url)
}
if (Test-Path $shortcutPath) {
try {
$wsh = New-Object -ComObject WScript.Shell
$shortcut = $wsh.CreateShortcut($shortcutPath)
Write-Host ""
Write-Info "Current shortcut content:"
Write-Kv -Key "Target" -Value $shortcut.TargetPath
Write-Kv -Key "Args" -Value $shortcut.Arguments
Write-Kv -Key "WorkDir" -Value $shortcut.WorkingDirectory
}
catch {
Write-Warn "Could not read shortcut details."
}
}
$launcherPath = Get-AutostartLauncherPath -App $App
if (Test-Path $launcherPath) {
Write-Host ""
Write-Info "Hidden launcher exists: $launcherPath"
}
Wait-User
}
function Invoke-AutostartAction {
param (
[hashtable]$App,
[switch]$SkipConfirm,
[switch]$NoPause
)
if ($SkipConfirm -and (Test-AutostartEnabled -App $App)) {
Write-Warn "Autostart already enabled, skipped: $($App.Name)"
return
}
if (-not $SkipConfirm) {
Clear-Host
Write-Title "Enable Autostart Confirmation"
Write-Kv -Key "Name" -Value $App.Name
Write-Kv -Key "Status" -Value (Get-AppStatusText -App $App)
Write-Kv -Key "Command" -Value (Get-AppCommandText -App $App)
Write-Host ""
$confirm = Read-Host "Enable or recreate autostart shortcut? Y/N"
if ($confirm -notmatch "^[Yy]$") {
Write-Warn "Cancelled."
if (-not $NoPause) {
Wait-User
}
return
}
}
$enabled = Enable-AutostartShortcut -App $App -NoPause
if ($enabled) {
Write-Host ""
Start-AutostartAppNow -App $App -NoPause
if ($App.Url) {
Write-Hint "Web UI: $($App.Url)"
}
}
if (-not $NoPause) {
Wait-User
}
}
function Show-AutostartActionMenu {
param (
[hashtable]$App
)
while ($true) {
Clear-Host
Write-Title "Autostart: $($App.Name)"
Write-Kv -Key "Status" -Value (Get-AppStatusText -App $App)
Write-Kv -Key "Shortcut" -Value (Get-AutostartShortcutPath -App $App)
Write-Kv -Key "Command" -Value (Get-AppCommandText -App $App)
Write-Host ""
Write-Option -Key "1" -Text "Enable / recreate autostart + start now (hidden)"
Write-Option -Key "2" -Text "Disable autostart"
Write-Option -Key "3" -Text "Start now (hidden)"
Write-Option -Key "4" -Text "Open Web UI"
Write-Option -Key "5" -Text "Show details"
Write-Option -Key "6" -Text "Stop now"
Write-Option -Key "B" -Text "Back"
Write-Option -Key "Q" -Text "Quit"
Write-Host ""
$choice = Read-Host "Choose"
switch -Regex ($choice) {
"^1$" {
Invoke-AutostartAction -App $App
}
"^2$" {
Disable-AutostartShortcut -App $App
}
"^3$" {
Start-AutostartAppNow -App $App
}
"^4$" {
Open-AutostartAppUrl -App $App
}
"^5$" {
Show-AutostartInfo -App $App
}
"^6$" {
Stop-AutostartAppNow -App $App
}
"^[Bb]$" {
return
}
"^[Qq]$" {
exit
}
default {
Write-Warn "Invalid input."
Wait-User
}
}
}
}
function Get-PackageDownloadsFolder {
$downloads = Join-Path $env:USERPROFILE "Downloads"
$folder = Join-Path $downloads "packages"
if (-not (Test-Path -LiteralPath $folder)) {
New-Item -ItemType Directory -Path $folder -Force | Out-Null
}
return $folder
}
function Get-SafeFileName {
param (
[string]$FileName
)
$invalidChars = [System.IO.Path]::GetInvalidFileNameChars()
$safeName = $FileName
foreach ($char in $invalidChars) {
$safeName = $safeName.Replace([string]$char, "_")
}
return $safeName
}
function Get-DownloadFileName {
param (
[hashtable]$App
)
if ($App.FileName -and -not [string]::IsNullOrWhiteSpace([string]$App.FileName)) {
return (Get-SafeFileName -FileName ([string]$App.FileName))
}
$url = [string]$App.Url
try {
$uri = [System.Uri]$url
$fileName = [System.IO.Path]::GetFileName($uri.AbsolutePath)
if (-not [string]::IsNullOrWhiteSpace($fileName)) {
return (Get-SafeFileName -FileName $fileName)
}
}
catch {
# Fall through to name-based fallback.
}
return (Get-SafeFileName -FileName "$($App.Name).zip")
}
function Get-DownloadDestinationFolder {
param (
[hashtable]$App
)
if ($App.DestinationDir -and -not [string]::IsNullOrWhiteSpace([string]$App.DestinationDir)) {
$folder = [Environment]::ExpandEnvironmentVariables([string]$App.DestinationDir)
}
else {
$folder = Get-PackageDownloadsFolder
}
if (-not (Test-Path -LiteralPath $folder)) {
New-Item -ItemType Directory -Path $folder -Force | Out-Null
}
return $folder
}
function Get-DownloadDestinationPath {
param (
[hashtable]$App
)
$folder = Get-DownloadDestinationFolder -App $App
$fileName = Get-DownloadFileName -App $App
return (Join-Path $folder $fileName)
}
function Test-DownloadFileExists {
param (
[hashtable]$App
)
$destinationPath = Get-DownloadDestinationPath -App $App
return (Test-Path -LiteralPath $destinationPath -PathType Leaf)
}
function Invoke-DownloadPackage {
param (
[hashtable]$App,
[switch]$SkipConfirm,
[switch]$NoPause
)
$url = [string]$App.Url
if ([string]::IsNullOrWhiteSpace($url)) {
Write-ErrorText "Download URL is empty: $($App.Name)"
if (-not $NoPause) {
Wait-User
}
return $false
}
$destinationPath = Get-DownloadDestinationPath -App $App
$destinationFolder = Split-Path -Parent $destinationPath
$fileExists = Test-Path -LiteralPath $destinationPath -PathType Leaf
if ($SkipConfirm -and $fileExists) {
Write-Warn "Already downloaded, skipped: $($App.Name)"
Write-Hint "Path: $destinationPath"
return $true
}
if ((-not $SkipConfirm) -and $fileExists) {
Write-Warn "File already exists: $destinationPath"
$confirm = Read-Host "Overwrite it? Y/N"
if ($confirm -notmatch "^[Yy]$") {
Write-Warn "Cancelled."
if (-not $NoPause) {
Wait-User
}
return $false
}
}
if (-not (Test-Path -LiteralPath $destinationFolder)) {
New-Item -ItemType Directory -Path $destinationFolder -Force | Out-Null
}
$tempPath = "$destinationPath.download"
if (Test-Path -LiteralPath $tempPath) {
Remove-Item -LiteralPath $tempPath -Force
}
Write-Host ""
Write-Info "Downloading: $($App.Name)"
Write-Hint "URL: $url"
Write-Hint "Save to: $destinationPath"
Write-Host ""
try {
Invoke-WebRequest -Uri $url -OutFile $tempPath -UseBasicParsing
if (Test-Path -LiteralPath $destinationPath) {
Remove-Item -LiteralPath $destinationPath -Force
}
Move-Item -LiteralPath $tempPath -Destination $destinationPath -Force
Write-Success "Downloaded: $destinationPath"
return $true
}
catch {
Write-ErrorText "Download failed: $($_.Exception.Message)"
if (Test-Path -LiteralPath $tempPath) {
Remove-Item -LiteralPath $tempPath -Force -ErrorAction SilentlyContinue
}
return $false
}
finally {
if (-not $NoPause) {
Wait-User
}
}
}
function Remove-DownloadedPackage {
param (
[hashtable]$App,
[switch]$NoPause
)
$destinationPath = Get-DownloadDestinationPath -App $App
if (-not (Test-Path -LiteralPath $destinationPath -PathType Leaf)) {
Write-Warn "Downloaded file not found: $destinationPath"
if (-not $NoPause) {
Wait-User
}
return $true
}
try {
Remove-Item -LiteralPath $destinationPath -Force
Write-Success "Deleted downloaded file: $destinationPath"
return $true
}
catch {
Write-ErrorText "Failed to delete file: $($_.Exception.Message)"
return $false
}
finally {
if (-not $NoPause) {
Wait-User
}
}
}
function Get-AppTypeText {
param (
[hashtable]$App
)
switch ($App.Type) {
"Winget" { return "Winget" }
"WslBase" { return "WSL Base" }
"WslDistro" { return "WSL Linux Distro" }
"Command" { return "Command" }
"Autostart" { return "Autostart" }
"Download" { return "Download" }
default { return "Unknown" }
}
}
function Get-AppCommandText {
param (
[hashtable]$App
)
switch ($App.Type) {
"Winget" {
return "winget install --id $($App.Id) -e"
}
"WslBase" {
return "$($App.Exe) $($App.Args -join ' ')"
}
"WslDistro" {
return "$($App.Exe) $($App.Args -join ' ')"
}
"Command" {
return "$($App.Exe) $($App.Args -join ' ')"
}
"Autostart" {
return "$($App.Command) $(Get-AutostartArgumentsText -App $App)"
}
"Download" {
return "Download to $(Get-DownloadDestinationPath -App $App)"
}
default {
return ""
}
}
}
function Get-AppStatusText {
param (
[hashtable]$App
)
switch ($App.Type) {
"Winget" {
if (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
return "Unknown, winget not found"
}
if (Test-WingetInstalled -Id ([string]$App.Id)) {
return "Installed"
}
return "Not installed"
}
"WslBase" {
if (Test-WslReady) {
return "Enabled"
}
return "Not enabled"
}
"WslDistro" {
if (-not (Test-WslReady)) {
return "WSL not ready"
}
if (Test-WslDistroInstalled -App $App) {
return "Installed"
}
return "Not installed"
}
"Command" {
return "Runnable command"
}
"Autostart" {
$enabledText = if (Test-AutostartEnabled -App $App) { "Enabled" } else { "Disabled" }
$runningText = if (Test-AutostartProcessRunning -App $App) { "Running" } else { "Not running" }
$exePath = Get-AutostartExePath -App $App
if ([string]::IsNullOrWhiteSpace($exePath)) {
return "$enabledText, command not found"
}
return "$enabledText, $runningText"
}
"Download" {
if (Test-DownloadFileExists -App $App) {
return "Downloaded"
}
return "Not downloaded"
}
default {
return "Unknown"
}
}
}
function Get-PrimaryActionText {
param (
[hashtable]$App
)
switch ($App.Type) {
"Winget" {
if (Test-WingetInstalled -Id ([string]$App.Id)) {
return "Upgrade / install again"
}
return "Install"
}
"WslBase" {
if (Test-WslReady) {
return "Already enabled"
}
return "Install"
}
"WslDistro" {
if (Test-WslDistroInstalled -App $App) {
return "Already installed"
}
return "Install"
}
"Command" {
return "Run"
}
"Autostart" {
if (Test-AutostartEnabled -App $App) {
return "Recreate autostart shortcut"
}
return "Enable autostart"
}
"Download" {
if (Test-DownloadFileExists -App $App) {
return "Download again / overwrite"
}
return "Download"
}
default {
return "Run"
}
}
}
function Convert-SelectionToNumbers {
param (
[string]$InputText,
[int]$MaxNumber
)
$InputText = $InputText.Trim()
if ($InputText -notmatch "^\d+(\s*-\s*\d+)?(\s*,\s*\d+(\s*-\s*\d+)?)*$") {
return $null
}
$result = @()
$parts = $InputText -split ","
foreach ($part in $parts) {
$part = $part.Trim()
if ($part -match "^(\d+)\s*-\s*(\d+)$") {
$start = [int]$Matches[1]
$end = [int]$Matches[2]
if ($start -gt $end) {
Write-Warn "Invalid range: $part"
return $null
}
for ($i = $start; $i -le $end; $i++) {
if ($i -lt 1 -or $i -gt $MaxNumber) {
Write-Warn "Invalid number: $i"
return $null
}
if ($result -notcontains $i) {
$result += $i
}
}
}
elseif ($part -match "^\d+$") {
$number = [int]$part
if ($number -lt 1 -or $number -gt $MaxNumber) {
Write-Warn "Invalid number: $number"
return $null
}
if ($result -notcontains $number) {
$result += $number
}
}
else {
return $null
}
}
return $result
}
function Install-WslBase {
param (
[switch]$NoPause
)
if (-not (Test-WslCommand)) {
if (-not $NoPause) {
Wait-User
}
return $false
}
if (Test-WslReady) {
Write-Success "WSL is already installed or enabled."
if (-not $NoPause) {
Wait-User
}
return $true
}
if (-not (Test-IsAdmin)) {
Restart-ScriptAsAdmin
return $false
}
Write-Host ""
Write-Info "Installing WSL base..."
Write-Hint "Command: wsl --install --no-distribution"
Write-Host ""
wsl --install --no-distribution
Write-Host ""
Write-Success "WSL base installation command finished."
Write-Warn "Please restart Windows, then run this script again to install Ubuntu/Debian/Kali/Arch."
if (-not $NoPause) {
Wait-User
}
return $false
}
function Ensure-WslReady {
param (
[switch]$NoPause
)
if (Test-WslReady) {
return $true
}
Clear-Host
Write-Title "WSL Required"
Write-Warn "WSL is not installed or enabled."
Write-Host ""
$confirm = Read-Host "Install WSL first? Y/N"
if ($confirm -notmatch "^[Yy]$") {
Write-Host ""
Write-Warn "Cancelled."
if (-not $NoPause) {
Wait-User
}
return $false
}
Install-WslBase -NoPause:$NoPause | Out-Null
return $false
}
function Test-AppSupportsUninstall {
param (
[hashtable]$App
)
switch ($App.Type) {
"Winget" { return $true }
"WslDistro" { return $true }
"Autostart" { return $true }
"Download" { return $true }
default { return $false }
}
}
function Get-AppUninstallCommandText {
param (
[hashtable]$App
)
switch ($App.Type) {
"Winget" {
return "winget uninstall --id $($App.Id) -e"
}
"WslDistro" {
$distroName = Get-WslDistroName -App $App
if ([string]::IsNullOrWhiteSpace($distroName)) {
return "wsl --unregister <distro>"
}
return "wsl --unregister $distroName"
}
"Autostart" {
return "Remove startup shortcut and hidden launcher"
}
"Download" {
return "Delete $(Get-DownloadDestinationPath -App $App)"
}
default {
return ""
}
}
}
function Get-RelatedAutostartApp {
param (
[hashtable]$App
)
$name = ""
if ($App.AutostartName) {
$name = [string]$App.AutostartName
}
else {
$name = [string]$App.Name
}
if ([string]::IsNullOrWhiteSpace($name)) {
return $null
}
if (-not $SoftwareGroups.Contains("Autostart")) {
return $null
}
foreach ($autostartApp in $SoftwareGroups["Autostart"]) {
if ([string]$autostartApp.Name -ieq $name) {
return $autostartApp
}
}
return $null
}
function Stop-AppProcessIfConfigured {
param (
[hashtable]$App,
[switch]$Silent
)
if (-not $App.ProcessName) {
return $true
}
$processName = [string]$App.ProcessName
$processes = Get-Process -Name $processName -ErrorAction SilentlyContinue
if ($null -eq $processes) {
if (-not $Silent) {
Write-Hint "Process is not running: $processName"
}
return $true
}
try {
$processes | Stop-Process -Force
if (-not $Silent) {
Write-Success "Stopped process: $processName"
}
return $true
}
catch {
if (-not $Silent) {
Write-Warn "Failed to stop process ${processName}: $($_.Exception.Message)"
}
return $false
}
}
function Invoke-AppUninstall {
param (
[hashtable]$App,
[switch]$SkipConfirm,
[switch]$NoPause
)
if (-not (Test-AppSupportsUninstall -App $App)) {
Write-Warn "Uninstall is not supported for this item: $($App.Name)"
if (-not $NoPause) {
Wait-User
}
return
}
$statusText = Get-AppStatusText -App $App
$uninstallCommand = Get-AppUninstallCommandText -App $App
if ($SkipConfirm) {
if ($App.Type -eq "Winget" -and $statusText -ne "Installed") {
Write-Warn "Not installed, skipped uninstall: $($App.Name)"
return
}
if ($App.Type -eq "WslDistro") {
Write-Warn "WSL distro uninstall requires single-item confirmation, skipped: $($App.Name)"
return
}
if ($App.Type -eq "Autostart" -and -not (Test-AutostartEnabled -App $App)) {
Write-Warn "Autostart not enabled, skipped: $($App.Name)"
return
}
if ($App.Type -eq "Download" -and -not (Test-DownloadFileExists -App $App)) {
Write-Warn "Downloaded file not found, skipped delete: $($App.Name)"
return
}
}
if (-not $SkipConfirm) {
Clear-Host
Write-Title "Uninstall Confirmation"
Write-Kv -Key "Name" -Value $App.Name
Write-Kv -Key "Type" -Value (Get-AppTypeText -App $App)
Write-Kv -Key "Status" -Value $statusText
Write-Kv -Key "Command" -Value $uninstallCommand
Write-Host ""
if ($App.Type -eq "Winget" -and $statusText -ne "Installed") {
Write-Warn "$($App.Name) is not installed."
Wait-User
return
}
if ($App.Type -eq "Download" -and -not (Test-DownloadFileExists -App $App)) {
Write-Warn "Downloaded file not found."
Wait-User
return
}
if ($App.Type -eq "WslDistro") {
Write-Warn "This will unregister the WSL distro and delete its Linux files."
Write-Warn "Only continue if you do not need data inside this distro."
Write-Host ""
}
if ($App.Type -eq "Winget" -and $App.ProcessName) {
Write-Hint "Before uninstalling, the script will stop its running process if needed."
}
if ($App.Type -eq "Winget" -and $App.AutostartName) {
Write-Hint "Related autostart shortcut will also be removed if it exists."
}
Write-Host ""
$confirm = Read-Host "Are you sure you want to uninstall $($App.Name)? Y/N"
if ($confirm -notmatch "^[Yy]$") {
Write-Warn "Cancelled."
if (-not $NoPause) {
Wait-User
}
return
}
}
$success = $true
if ($App.Type -eq "Winget") {
if (-not (Test-Winget)) {
if (-not $NoPause) {
Wait-User
}
return
}
$relatedAutostartApp = Get-RelatedAutostartApp -App $App
if ($null -ne $relatedAutostartApp) {
Disable-AutostartShortcut -App $relatedAutostartApp -NoPause
}
Stop-AppProcessIfConfigured -App $App | Out-Null
$id = [string]$App.Id
Write-Host ""
Write-Info "Uninstalling: $($App.Name)"
Write-Hint "Winget ID: $id"
Write-Host ""
winget uninstall --id $id -e
if ($LASTEXITCODE -ne 0) {
$success = $false
}
}
elseif ($App.Type -eq "WslDistro") {
if (-not (Test-WslCommand)) {
if (-not $NoPause) {
Wait-User
}
return
}
$distroName = Get-WslDistroName -App $App
if ([string]::IsNullOrWhiteSpace($distroName)) {
Write-ErrorText "Could not determine WSL distro name."
$success = $false
}
elseif (-not (Test-WslDistroInstalled -App $App)) {
Write-Warn "WSL distro is not installed: $($App.Name)"
$success = $true
}
else {
Write-Host ""
Write-Info "Unregistering WSL distro: $distroName"
Write-Hint "Command: wsl --unregister $distroName"
Write-Host ""
wsl --unregister $distroName
if ($LASTEXITCODE -ne 0) {
$success = $false
}
}
}
elseif ($App.Type -eq "Autostart") {
Disable-AutostartShortcut -App $App -NoPause
$success = $true
}
elseif ($App.Type -eq "Download") {
$success = Remove-DownloadedPackage -App $App -NoPause
}
Write-Host ""
if ($success) {
Write-Success "Uninstall finished: $($App.Name)"
}
else {
Write-Warn "Uninstall finished with errors: $($App.Name)"
Write-Hint "Exit code: $LASTEXITCODE"
}
if (-not $NoPause) {
Wait-User
}
}
function Show-AppInfo {
param (
[hashtable]$App
)
Clear-Host
Write-Title "Software Info"
Write-Kv -Key "Name" -Value $App.Name
Write-Kv -Key "Type" -Value (Get-AppTypeText -App $App)
Write-Kv -Key "Status" -Value (Get-AppStatusText -App $App)
Write-Kv -Key "Command" -Value (Get-AppCommandText -App $App)
if (Test-AppSupportsUninstall -App $App) {
Write-Kv -Key "Uninstall" -Value (Get-AppUninstallCommandText -App $App)
}
Write-Host ""
if ($App.Type -eq "Winget") {
if (Test-Winget) {
$id = [string]$App.Id
winget show --id $id -e
}
}
elseif ($App.Type -eq "WslBase" -or $App.Type -eq "WslDistro") {
if (Test-WslCommand) {
Write-Host ""
Write-Info "WSL status:"
wsl --status
Write-Host ""
Write-Info "Installed WSL distributions:"
wsl --list --verbose
Write-Host ""
Write-Info "Available online distributions:"
wsl --list --online
}
}
elseif ($App.Type -eq "Command") {
Write-Hint "This item runs a command directly."
}
elseif ($App.Type -eq "Autostart") {
Write-Hint "This item creates or removes a shortcut in the current user's Startup folder."
Write-Host ""
Write-Info "Autostart details:"
Write-Kv -Key "Startup" -Value (Get-StartupFolder)
Write-Kv -Key "Shortcut" -Value (Get-AutostartShortcutPath -App $App)
Write-Kv -Key "Running" -Value ($(if (Test-AutostartProcessRunning -App $App) { "Running" } else { "Not running" }))
if ($App.Url) {
Write-Kv -Key "Web UI" -Value ([string]$App.Url)
}
}
elseif ($App.Type -eq "Download") {
Write-Hint "This item downloads a file to Downloads\packages by default."
Write-Host ""
Write-Info "Download details:"
Write-Kv -Key "URL" -Value ([string]$App.Url)
Write-Kv -Key "File" -Value (Get-DownloadDestinationPath -App $App)
Write-Kv -Key "Exists" -Value ($(if (Test-DownloadFileExists -App $App) { "Yes" } else { "No" }))
}
else {
Write-Warn "Unknown app type."
}
Wait-User
}
function Invoke-AppAction {
param (
[hashtable]$App,
[switch]$SkipConfirm,
[switch]$NoPause
)
$actionText = Get-PrimaryActionText -App $App
$statusText = Get-AppStatusText -App $App
if ($SkipConfirm) {
if ($App.Type -eq "Winget" -and $statusText -eq "Installed") {
Write-Warn "Already installed, skipped: $($App.Name)"
return
}
if ($App.Type -eq "WslBase" -and $statusText -eq "Enabled") {
Write-Warn "WSL already enabled, skipped."
return
}
if ($App.Type -eq "WslDistro" -and $statusText -eq "Installed") {
Write-Warn "Already installed, skipped: $($App.Name)"
return
}
if ($App.Type -eq "Autostart" -and (Test-AutostartEnabled -App $App)) {
Write-Warn "Autostart already enabled, skipped: $($App.Name)"
return
}
if ($App.Type -eq "Download" -and (Test-DownloadFileExists -App $App)) {
Write-Warn "Already downloaded, skipped: $($App.Name)"
Write-Hint "Path: $(Get-DownloadDestinationPath -App $App)"
return
}
}
if (-not $SkipConfirm) {
Clear-Host
Write-Title "$actionText Confirmation"
Write-Kv -Key "Name" -Value $App.Name
Write-Kv -Key "Type" -Value (Get-AppTypeText -App $App)
Write-Kv -Key "Status" -Value $statusText
Write-Kv -Key "Command" -Value (Get-AppCommandText -App $App)
Write-Host ""
if ($App.Type -eq "Winget" -and $statusText -eq "Installed") {
$confirm = Read-Host "Already installed. Continue and run winget upgrade/install? Y/N"
}
elseif ($App.Type -eq "WslBase" -and $statusText -eq "Enabled") {
Write-Success "WSL is already enabled."
Wait-User
return
}
elseif ($App.Type -eq "WslDistro" -and $statusText -eq "Installed") {
Write-Success "$($App.Name) is already installed."
Wait-User
return
}
elseif ($App.Type -eq "Command") {
$confirm = Read-Host "Are you sure you want to run $($App.Name)? Y/N"
}
elseif ($App.Type -eq "Download") {
$confirm = Read-Host "Are you sure you want to download $($App.Name)? Y/N"
}
else {
$confirm = Read-Host "Are you sure you want to install $($App.Name)? Y/N"
}
if ($confirm -notmatch "^[Yy]$") {
Write-Host ""
Write-Warn "Cancelled."
if (-not $NoPause) {
Wait-User
}
return
}
}
$success = $true
if ($App.Type -eq "Winget") {
if (-not (Test-Winget)) {
if (-not $NoPause) {
Wait-User
}
return
}
$id = [string]$App.Id
$isInstalled = Test-WingetInstalled -Id $id
Write-Host ""
if ($isInstalled) {
Write-Info "Upgrading: $($App.Name)"
Write-Hint "Winget ID: $id"
Write-Host ""
winget upgrade --id $id -e --accept-source-agreements --accept-package-agreements
if ($LASTEXITCODE -ne 0) {
Write-Warn "Upgrade command did not complete successfully. Trying install command..."
winget install --id $id -e --accept-source-agreements --accept-package-agreements
}
}
else {
Write-Info "Installing: $($App.Name)"
Write-Hint "Winget ID: $id"
Write-Host ""
winget install --id $id -e --accept-source-agreements --accept-package-agreements
}
if ($LASTEXITCODE -ne 0) {
$success = $false
}
}
elseif ($App.Type -eq "WslBase") {
Install-WslBase -NoPause:$NoPause | Out-Null
return
}
elseif ($App.Type -eq "WslDistro") {
if (-not (Ensure-WslReady -NoPause:$NoPause)) {
return
}
if (-not (Test-IsAdmin)) {
Restart-ScriptAsAdmin
return
}
$exe = [string]$App.Exe
$cmdArgs = @($App.Args)
Write-Host ""
Write-Info "Installing: $($App.Name)"
Write-Hint "Command: $(Get-AppCommandText -App $App)"
Write-Host ""
& $exe @cmdArgs
if ($LASTEXITCODE -ne 0) {
$success = $false
}
}
elseif ($App.Type -eq "Autostart") {
Invoke-AutostartAction -App $App -SkipConfirm:$SkipConfirm -NoPause:$NoPause
return
}
elseif ($App.Type -eq "Download") {
$success = Invoke-DownloadPackage -App $App -SkipConfirm:$SkipConfirm -NoPause:$NoPause
}
elseif ($App.Type -eq "Command") {
$exe = [string]$App.Exe
$cmdArgs = @($App.Args)
if (-not (Test-CommandExists -CommandName $exe)) {
if (-not $NoPause) {
Wait-User
}
return
}
Write-Host ""
Write-Info "Running: $($App.Name)"
Write-Hint "Command: $(Get-AppCommandText -App $App)"
Write-Host ""
& $exe @cmdArgs
if ($LASTEXITCODE -ne 0) {
$success = $false
}
}
else {
Write-Warn "Unknown app type."
$success = $false
}
Write-Host ""
if ($success) {
Write-Success "Finished: $($App.Name)"
}
else {
Write-Warn "Command finished with errors: $($App.Name)"
Write-Hint "Exit code: $LASTEXITCODE"
}
if (-not $NoPause) {
Wait-User
}
}
function Show-SingleAppActionMenu {
param (
[hashtable]$App
)
if ($App.Type -eq "Autostart") {
Show-AutostartActionMenu -App $App
return
}
while ($true) {
Clear-Host
Write-Title "Selected Software"
Write-Kv -Key "Name" -Value $App.Name
Write-Kv -Key "Type" -Value (Get-AppTypeText -App $App)
Write-Kv -Key "Status" -Value (Get-AppStatusText -App $App)
Write-Kv -Key "Command" -Value (Get-AppCommandText -App $App)
Write-Host ""
Write-Option -Key "1" -Text (Get-PrimaryActionText -App $App)
Write-Option -Key "2" -Text "Show info"
if (Test-AppSupportsUninstall -App $App) {
Write-Option -Key "3" -Text "Uninstall"
}
Write-Option -Key "B" -Text "Back"
Write-Option -Key "Q" -Text "Quit"
Write-Host ""
$choice = Read-Host "Choose"
switch -Regex ($choice) {
"^1$" {
Invoke-AppAction -App $App
}
"^2$" {
Show-AppInfo -App $App
}
"^3$" {
if (Test-AppSupportsUninstall -App $App) {
Invoke-AppUninstall -App $App
}
else {
Write-Warn "Uninstall is not supported for this item."
Wait-User
}
}
"^[Bb]$" {
return
}
"^[Qq]$" {
exit
}
default {
Write-Warn "Invalid input."
Wait-User
}
}
}
}
function Show-MultiAppActionMenu {
param (
[array]$SelectedApps
)
while ($true) {
Clear-Host
Write-Title "Selected Software List"
for ($i = 0; $i -lt $SelectedApps.Count; $i++) {
$app = $SelectedApps[$i]
$typeText = Get-AppTypeText -App $app
$statusText = Get-AppStatusText -App $app
Write-MenuItem -Number ($i + 1) -Name $app.Name -Extra "[$typeText] [$statusText]"
}
Write-Host ""
Write-Option -Key "1" -Text "Install selected, skip installed"
Write-Option -Key "2" -Text "Show info one by one"
Write-Option -Key "3" -Text "Uninstall selected, skip unsupported/not installed"
Write-Option -Key "B" -Text "Back"
Write-Option -Key "Q" -Text "Quit"
Write-Host ""
Write-Hint "Installed software / WSL distros / enabled autostart items / downloaded files will be skipped."
Write-Hint "Maintenance commands will still run."
Write-Hint "Batch uninstall skips WSL distros. Unregister WSL distros from the single-item menu."
Write-Host ""
$choice = Read-Host "Choose"
switch -Regex ($choice) {
"^1$" {
Write-Host ""
$confirm = Read-Host "Install selected items and skip installed ones? Y/N"
if ($confirm -notmatch "^[Yy]$") {
Write-Warn "Cancelled."
Wait-User
return
}
foreach ($app in $SelectedApps) {
Invoke-AppAction -App $app -SkipConfirm -NoPause
}
Wait-User
}
"^2$" {
foreach ($app in $SelectedApps) {
Show-AppInfo -App $app
}
}
"^3$" {
Write-Host ""
Write-Warn "This will uninstall selected winget packages, remove selected autostart items, and delete selected downloaded files."
Write-Hint "WSL distros are skipped in batch uninstall to avoid accidental data loss."
$confirm = Read-Host "Uninstall selected items? Y/N"
if ($confirm -notmatch "^[Yy]$") {
Write-Warn "Cancelled."
Wait-User
return
}
foreach ($app in $SelectedApps) {
Invoke-AppUninstall -App $app -SkipConfirm -NoPause
}
Wait-User
}
"^[Bb]$" {
return
}
"^[Qq]$" {
exit
}
default {
Write-Warn "Invalid input."
Wait-User
}
}
}
}
function Show-GroupMenu {
param (
[string]$GroupName,
[array]$Apps
)
while ($true) {
Clear-Host
Write-Title "Category: $GroupName"
for ($i = 0; $i -lt $Apps.Count; $i++) {
$app = $Apps[$i]
Write-MenuItem -Number ($i + 1) -Name $app.Name -Extra "[$(Get-AppTypeText -App $app)]"
}
Write-Host ""
Write-Option -Key "B" -Text "Back"
Write-Option -Key "Q" -Text "Quit"
Write-Host ""
Write-Hint "Input examples:"
Write-Hint " 1 single software"
Write-Hint " 1-3 range"
Write-Hint " 2,3,6 multiple software"
Write-Hint " 1-5,8,10 mixed range and numbers"
Write-Host ""
$choice = Read-Host "Choose software"
switch -Regex ($choice) {
"^[Qq]$" {
exit
}
"^[Bb]$" {
return
}
default {
$numbers = Convert-SelectionToNumbers -InputText $choice -MaxNumber $Apps.Count
if ($null -eq $numbers) {
Write-Warn "Invalid input."
Wait-User
continue
}
$selectedApps = @()
foreach ($number in $numbers) {
$selectedApps += $Apps[$number - 1]
}
if ($selectedApps.Count -eq 1) {
Show-SingleAppActionMenu -App $selectedApps[0]
}
else {
Show-MultiAppActionMenu -SelectedApps $selectedApps
}
}
}
}
}
function Show-MainMenu {
$groups = @($SoftwareGroups.Keys)
while ($true) {
Clear-Host
Write-Title "Software Install Menu"
for ($i = 0; $i -lt $groups.Count; $i++) {
Write-MenuItem -Number ($i + 1) -Name $groups[$i]
}
Write-Host ""
Write-Option -Key "Q" -Text "Quit"
Write-Host ""
$choice = Read-Host "Choose category"
if ($choice -match "^[Qq]$") {
exit
}
if ($choice -match "^\d+$") {
$number = [int]$choice
if ($number -ge 1 -and $number -le $groups.Count) {
$groupName = $groups[$number - 1]
Show-GroupMenu -GroupName $groupName -Apps $SoftwareGroups[$groupName]
}
else {
Write-Warn "Invalid category number."
Wait-User
}
}
else {
Write-Warn "Invalid input."
Wait-User
}
}
}
Show-MainMenu