# Скрипт: gperm.ps1 # Версия: 1.5.0 # Дата: 2026-05-31 # Кириллица и спецсимволы - только внутри here-string. # При неверной кодировке (irm без charset) скрипт сам перезагружается через WebClient+UTF8. $_URL = 'https://ps.gper.ru/gperm.ps1' # ================================================================ # ВСЕ не-ASCII строки изолированы в here-string. # Парсер PS не разбирает содержимое @'...'@ - поэтому даже # при искажённой кодировке скрипт проходит парсинг без ошибок. # ================================================================ $_CYR = @' #CHECK:OK $_BH='═'; $_BV='║'; $_BTL='╔'; $_BTR='╗'; $_BBL='╚'; $_BBR='╝' $_BML='╠'; $_BMR='╣'; $_BMT='╦'; $_BMB='╩' $_TTitle = 'GPER │ Установка ПО' $_TMain = 'Главное меню' $_TEmpty = 'Space - отметить, I - установить, Q - выйти' $_TCount = 'Отмечено:' $_TInstBtn = '[I - установить]' $_TWill = 'Будет установлено' $_TConfirm = 'Продолжить? (Y/N) ' $_TDone = 'Готово. Нажмите любую клавишу...' $_TNone = 'Ничего не отмечено.' $_THelp = ' ↑↓ навигация Tab панель →/Enter войти ←/Esc назад Space выбор I уст. Q выход' $MENU = @( @{ N='Система'; C=@( @{ N='Winget (Win10)'; Script='https://ps.gper.ru/ext/winget-install.ps1'; Note='asheroto/winget-install' } @{ N='PowerShell 7'; Id='Microsoft.PowerShell' } @{ N='Windows Terminal'; Id='Microsoft.WindowsTerminal' } )} @{ N='Удалённый доступ'; C=@( @{ N='Radmin Viewer'; Id='Famatech.Radmin.Viewer' } @{ N='Radmin Server (install)'; Script='https://ps.gper.ru/ext/radmin-install.ps1'; Note='установка + регистрация' } @{ N='Radmin Server (register)'; Script='https://ps.gper.ru/ext/radmin-register.ps1'; Note='только регистрация (патч)' } @{ N='RealVNC Viewer'; Id='RealVNC.Viewer' } @{ N='RealVNC Server'; Id='RealVNC.Server' } @{ N='RealVNC Server (register)'; Script='https://ps.gper.ru/vnc.ps1'; Note='регистрация / патч' } @{ N='AnyDesk'; Id='AnyDeskSoftwareGmbH.AnyDesk' } @{ N='RDP'; C=@( @{ N='RDP Wrapper'; Id=$null; Note='github.com/stascorp/rdpwrap' } @{ N='RDP Protect'; Id=$null; Note='manual install' } )} )} @{ N='Браузеры'; C=@( @{ N='Chrome'; Id='Google.Chrome' } @{ N='Firefox'; Id='Mozilla.Firefox' } @{ N='Яндекс Браузер'; Id='Yandex.Browser' } @{ N='Opera'; Id='Opera.Opera' } )} @{ N='Архиваторы'; C=@( @{ N='7-Zip'; Id='7zip.7zip' } @{ N='WinRAR'; Id='RARLab.WinRAR' } )} @{ N='Утилиты'; C=@( @{ N='Far Manager'; Id='FarGroup.FarManager' } @{ N='Everything'; Id='voidtools.Everything' } @{ N='Process Hacker'; Id='ProcessHacker.ProcessHacker' } @{ N='HWiNFO64'; Id='REALiX.HWiNFO' } @{ N='CPU-Z'; Id='CPUID.CPU-Z' } @{ N='CrystalDiskInfo'; Id='CrystalDewWorld.CrystalDiskInfo' } )} @{ N='Мессенджеры'; C=@( @{ N='Telegram'; Id='Telegram.TelegramDesktop' } @{ N='WhatsApp'; Id='WhatsApp.WhatsApp' } @{ N='Skype'; Id='Microsoft.Skype' } )} @{ N='Разработка'; C=@( @{ N='Git'; Id='Git.Git' } @{ N='VS Code'; Id='Microsoft.VisualStudioCode' } @{ N='Notepad++'; Id='Notepad++.Notepad++' } @{ N='Python 3'; Id='Python.Python.3' } @{ N='Node.js LTS'; Id='OpenJS.NodeJS.LTS' } @{ N='Docker Desktop'; Id='Docker.DockerDesktop' } )} ) '@ # Sentinel: если кириллица искажена - перезагружаем через WebClient+UTF8 if ($_CYR -notmatch '(?m)^#CHECK:OK') { $wc = New-Object System.Net.WebClient $wc.Encoding = [System.Text.Encoding]::UTF8 Invoke-Expression $wc.DownloadString($_URL) return } Invoke-Expression $_CYR # ================================================================ # Далее - только ASCII. Кириллица только через переменные выше. # ================================================================ $WL = 22 $WR = 28 $WT = $WL + $WR + 5 # Минимумы - фактический размер пересчитывается в Render под содержимое и ширину окна. $WL_MIN = 18 $WR_MIN = 24 $st = @{ Stack = New-Object 'System.Collections.Generic.Stack[hashtable]' Level = $MENU Prefix = '' LIdx = 0 RIdx = 0 Focus = 'L' Checked = New-Object 'System.Collections.Generic.HashSet[string]' } function IsLeaf($n) { -not $n.ContainsKey('C') } function IsCat($n) { $n.ContainsKey('C') } function RItems { if ($st.Level.Count -eq 0 -or $st.LIdx -ge $st.Level.Count) { return @() } $sel = $st.Level[$st.LIdx] if (IsLeaf $sel) { return @() } return @($sel.C | Where-Object { IsLeaf $_ }) } function LKey([int]$i) { return "$($st.Prefix)$($st.Level[$i].N)" } function RKey([int]$i) { if ($st.LIdx -ge $st.Level.Count) { return '' } return "$($st.Prefix)$($st.Level[$st.LIdx].N) > $((RItems)[$i].N)" } function GoBack { if ($st.Stack.Count -gt 0) { $prev = $st.Stack.Pop() $st.Level = $prev.Level $st.Prefix = $prev.Prefix $st.LIdx = $prev.LIdx $st.RIdx = $prev.RIdx $st.Focus = $prev.Focus } } function DrillIn { $sel = $st.Level[$st.LIdx] if (IsLeaf $sel) { return } $st.Stack.Push(@{Level = $st.Level; Prefix = $st.Prefix; LIdx = $st.LIdx; RIdx = $st.RIdx; Focus = $st.Focus }) $st.Prefix = "$($st.Prefix)$($sel.N) > " $st.Level = $sel.C $st.LIdx = 0; $st.RIdx = 0; $st.Focus = 'L' } function ToggleKey([string]$k) { if ($st.Checked.Contains($k)) { $null = $st.Checked.Remove($k) } else { $null = $st.Checked.Add($k) } } function ToggleL { $node = $st.Level[$st.LIdx] if (IsLeaf $node) { ToggleKey (LKey $st.LIdx) } else { $cp = "$($st.Prefix)$($node.N) > " $leaves = @($node.C | Where-Object { IsLeaf $_ }) $allOn = ($leaves | Where-Object { $st.Checked.Contains("$cp$($_.N)") }).Count -eq $leaves.Count foreach ($leaf in $leaves) { $k = "$cp$($leaf.N)" if ($allOn) { $null = $st.Checked.Remove($k) } else { $null = $st.Checked.Add($k) } } } } function ToggleR { $r = RItems if ($r.Count -gt 0 -and $st.RIdx -lt $r.Count) { ToggleKey (RKey $st.RIdx) } } function Pad([string]$s, [int]$n) { if ($s.Length -ge $n) { return $s.Substring(0, $n) } return $s.PadRight($n) } function Render { $l = $st.Level $r = RItems $rows = [Math]::Max([Math]::Max($l.Count, $r.Count), 1) $cnt = $st.Checked.Count $crumb = if ($st.Prefix) { $st.Prefix.TrimEnd(' ').TrimEnd('>').TrimEnd(' ') } else { $_TMain } $status = if ($cnt -gt 0) { "$_TCount $cnt $_TInstBtn" } else { $_TEmpty } # --- Авто-ширина колонок: подгон под самые длинные имена + рамки/префикс --- # Префикс рендера: " >[x] " = 6, " " справа = 1 → запас 7 на колонку $lMax = 0; foreach ($n in $l) { if ($n.N.Length -gt $lMax) { $lMax = $n.N.Length } } $rMax = 0; foreach ($n in $r) { if ($n.N.Length -gt $rMax) { $rMax = $n.N.Length } } $WL = [Math]::Max($WL_MIN, $lMax + 7) $WR = [Math]::Max($WR_MIN, $rMax + 7) # Не вылазим за ширину окна (рамки: левый WL + " " + правый WR+1 + 2 рамки + 2 поля = WL+WR+5) $maxW = [Console]::BufferWidth - 1 if ($WL + $WR + 5 -gt $maxW) { $over = ($WL + $WR + 5) - $maxW # ужимаем равномерно, но не ниже минимумов $shrinkL = [Math]::Min([Math]::Floor($over / 2), $WL - $WL_MIN) $shrinkR = $over - $shrinkL if ($shrinkR -gt ($WR - $WR_MIN)) { $extra = $shrinkR - ($WR - $WR_MIN); $shrinkR = $WR - $WR_MIN $shrinkL += $extra } $WL -= $shrinkL; $WR -= $shrinkR if ($WL -lt $WL_MIN) { $WL = $WL_MIN } if ($WR -lt $WR_MIN) { $WR = $WR_MIN } } $WT = $WL + $WR + 5 # Очистка при изменении размеров - иначе старая рамка остаётся на экране if ($st.PrevWT -and ($st.PrevWT -ne $WT -or $st.PrevRows -ne $rows)) { [Console]::Clear() } $st.PrevWT = $WT $st.PrevRows = $rows $sb = [System.Text.StringBuilder]::new() $null = $sb.AppendLine("$_BTL$($_BH * ($WT-2))$_BTR") $null = $sb.AppendLine("$_BV$(Pad " $_TTitle" ($WT-2))$_BV") $null = $sb.AppendLine("$_BV$(Pad " $crumb" ($WT-2))$_BV") $null = $sb.AppendLine("$_BML$($_BH * $WL)$_BMT$($_BH * ($WR+2))$_BMR") for ($i = 0; $i -lt $rows; $i++) { $lc = ' ' * $WL if ($i -lt $l.Count) { $node = $l[$i] $sel = ($i -eq $st.LIdx) if (IsLeaf $node) { $chk = if ($st.Checked.Contains((LKey $i))) { 'x' } else { ' ' } $pfx = if ($sel -and $st.Focus -eq 'L') { ">[$chk]" } else { " [$chk]" } $lc = Pad " $pfx $($node.N)" $WL } else { $hasSub = ($node.C | Where-Object { IsCat $_ }).Count -gt 0 $arr = if ($hasSub) { '>' } else { '>' } $pfx = if ($sel -and $st.Focus -eq 'L') { " $arr" } else { ' ' } $lc = Pad " $pfx $($node.N)" $WL } } $rc = ' ' * ($WR + 1) if ($i -lt $r.Count) { $node = $r[$i] $chk = if ($st.Checked.Contains((RKey $i))) { 'x' } else { ' ' } if ($i -eq $st.RIdx -and $st.Focus -eq 'R') { $rc = Pad " >[$chk] $($node.N)" ($WR + 1) } else { $rc = Pad " [$chk] $($node.N)" ($WR + 1) } } $null = $sb.AppendLine("$_BV$lc $_BV$rc$_BV") } $null = $sb.AppendLine("$_BML$($_BH * $WL)$_BMB$($_BH * ($WR+2))$_BMR") $null = $sb.AppendLine("$_BV$(Pad " $status" ($WT-2))$_BV") $null = $sb.AppendLine("$_BML$($_BH * ($WT-2))$_BMR") $null = $sb.AppendLine("$_BV$(Pad $_THelp ($WT-2))$_BV") $null = $sb.Append( "$_BBL$($_BH * ($WT-2))$_BBR") [Console]::SetCursorPosition(0, 0) [Console]::Write($sb.ToString()) } function Collect-Checked($nodes, [string]$pfx = '') { $result = @() foreach ($n in $nodes) { if (IsLeaf $n) { $k = "$pfx$($n.N)" if ($st.Checked.Contains($k)) { $result += @{ Key = $k; Node = $n } } } else { $cp = "$pfx$($n.N) > " foreach ($child in ($n.C | Where-Object { IsLeaf $_ })) { $k = "$cp$($child.N)" if ($st.Checked.Contains($k)) { $result += @{ Key = $k; Node = $child } } } $result += Collect-Checked ($n.C | Where-Object { IsCat $_ }) $cp } } $result } function Install-Checked { [Console]::Clear() [Console]::CursorVisible = $true $items = @(Collect-Checked $MENU) if ($items.Count -eq 0) { Microsoft.PowerShell.Utility\Write-Host " $_TNone" -ForegroundColor Yellow Start-Sleep -Milliseconds 1500 [Console]::CursorVisible = $false; [Console]::Clear(); return } Microsoft.PowerShell.Utility\Write-Host '' Microsoft.PowerShell.Utility\Write-Host " $_TWill ($($items.Count)):" -ForegroundColor Cyan Microsoft.PowerShell.Utility\Write-Host '' foreach ($it in $items) { $item = $it.Node $tag = if ($item.Id) { '[winget]' } elseif ($item.Script) { '[script]' } else { '[manual]' } Microsoft.PowerShell.Utility\Write-Host " $tag $($item.N)" -ForegroundColor DarkCyan } Microsoft.PowerShell.Utility\Write-Host '' Microsoft.PowerShell.Utility\Write-Host " $_TConfirm" -ForegroundColor White -NoNewline $c = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown') Microsoft.PowerShell.Utility\Write-Host '' if ($c.Character -ne 'y' -and $c.Character -ne 'Y') { [Console]::CursorVisible = $false; [Console]::Clear(); return } Microsoft.PowerShell.Utility\Write-Host '' $okCount = 0; $failCount = 0 foreach ($it in $items) { $item = $it.Node $ok = $false if ($item.Id) { Microsoft.PowerShell.Utility\Write-Host " [>] $($item.N)" -ForegroundColor Cyan & winget install -e --id $item.Id --accept-source-agreements --accept-package-agreements # winget: 0 = OK, -1978335189 (0x8A15002B) = уже установлен - тоже считаем успехом if ($LASTEXITCODE -eq 0 -or $LASTEXITCODE -eq -1978335189) { $ok = $true } } elseif ($item.Script) { Microsoft.PowerShell.Utility\Write-Host " [>] $($item.N) ($($item.Script))" -ForegroundColor Cyan try { $code = (New-Object System.Net.WebClient).DownloadString($item.Script) Invoke-Expression $code $ok = $true } catch { Microsoft.PowerShell.Utility\Write-Host " [X] Ошибка: $($_.Exception.Message)" -ForegroundColor Red } } else { Microsoft.PowerShell.Utility\Write-Host " [!] $($item.N): $($item.Note)" -ForegroundColor Yellow # manual - отметку не снимаем, требует ручных действий } if ($ok) { $null = $st.Checked.Remove($it.Key) $okCount++ Microsoft.PowerShell.Utility\Write-Host " [OK] $($item.N)" -ForegroundColor Green } elseif ($item.Id -or $item.Script) { $failCount++ Microsoft.PowerShell.Utility\Write-Host " [FAIL] $($item.N)" -ForegroundColor Red } Microsoft.PowerShell.Utility\Write-Host '' } Microsoft.PowerShell.Utility\Write-Host " $_TDone (OK: $okCount, FAIL: $failCount)" -ForegroundColor Green $null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown') [Console]::CursorVisible = $false; [Console]::Clear() } $origTitle = $Host.UI.RawUI.WindowTitle $origCursor = [Console]::CursorVisible [Console]::Clear() [Console]::CursorVisible = $false $Host.UI.RawUI.WindowTitle = 'GPER' try { $running = $true while ($running) { Render $key = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown') $vk = [int]$key.VirtualKeyCode $ch = $key.Character switch ($vk) { 38 { if ($st.Focus -eq 'L') { if ($st.LIdx -gt 0) { $st.LIdx--; $st.RIdx = 0 } } else { if ($st.RIdx -gt 0) { $st.RIdx-- } } } 40 { if ($st.Focus -eq 'L') { if ($st.LIdx -lt $st.Level.Count-1) { $st.LIdx++; $st.RIdx = 0 } } else { $r = RItems; if ($st.RIdx -lt $r.Count-1) { $st.RIdx++ } } } 9 { $r = RItems if ($r.Count -gt 0) { $st.Focus = if ($st.Focus -eq 'L') {'R'} else {'L'} } } 13 { if ($st.Focus -eq 'L') { $sel = $st.Level[$st.LIdx] if (IsCat $sel) { if (($sel.C | Where-Object { IsCat $_ }).Count -gt 0) { DrillIn } else { $r = RItems; if ($r.Count -gt 0) { $st.Focus = 'R' } } } else { ToggleL } } else { ToggleR } } 39 { if ($st.Focus -eq 'L') { $sel = $st.Level[$st.LIdx] if (IsCat $sel) { if (($sel.C | Where-Object { IsCat $_ }).Count -gt 0) { DrillIn } else { $r = RItems; if ($r.Count -gt 0) { $st.Focus = 'R' } } } } } 37 { if ($st.Focus -eq 'R') { $st.Focus = 'L' } else { GoBack } } 27 { if ($st.Focus -eq 'R') { $st.Focus = 'L' } elseif ($st.Stack.Count -gt 0) { GoBack } else { $running = $false } } 32 { if ($st.Focus -eq 'L') { ToggleL } else { ToggleR } } } if ($ch -eq 'i' -or $ch -eq 'I') { Install-Checked } if ($ch -eq 'q' -or $ch -eq 'Q') { $running = $false } } } catch { [Console]::CursorVisible = $origCursor [Console]::Clear() $Host.UI.RawUI.WindowTitle = $origTitle Write-Host "" Write-Host "[X] ОШИБКА в gperm.ps1:" -ForegroundColor Red Write-Host $_.Exception.Message -ForegroundColor Yellow Write-Host "" Write-Host "Stack:" -ForegroundColor DarkGray Write-Host $_.ScriptStackTrace -ForegroundColor DarkGray Write-Host "" Write-Host "Нажмите любую клавишу..." -ForegroundColor DarkGray $null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown') } finally { [Console]::CursorVisible = $origCursor $Host.UI.RawUI.WindowTitle = $origTitle }