#Requires -Version 5.1 Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' # --- Кодировка вывода и TLS 1.2 (нужен на старых системах) --- [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch { } # --- Константы --- $SelfUrl = 'https://lib.genius8loci.one/lib.ps1' $ApiUrl = 'https://lib.genius8loci.one/api/gists' $DescWidth = 60 # ширина колонки описания $NameWidth = 20 # ширина колонки имени файла $Colors = @{ Title='Magenta'; Menu='Cyan'; Error='Red'; Index='Green'; FileName='Yellow'; Hint='DarkGray'; SelFg='Black'; SelBg='Magenta' } $Banner = @( ' ██████╗███╗ ███╗██████╗ ██╗ ██╗██████╗ ' ' ██╔════╝████╗ ████║██╔══██╗ ██║ ██║██╔══██╗' ' ██║ ██╔████╔██║██║ ██║ ██║ ██║██████╔╝' ' ██║ ██║╚██╔╝██║██║ ██║ ██║ ██║██╔══██╗' ' ╚██████╗██║ ╚═╝ ██║██████╔╝ ███████╗██║██████╔╝' ' ╚═════╝╚═╝ ╚═╝╚═════╝ ╚══════╝╚═╝╚═════╝ ' '' ' ██████╗ ███████╗███╗ ██╗██╗██╗ ██╗███████╗ █████╗ ██╗ ██████╗ ██████╗██╗' ' ██╔════╝ ██╔════╝████╗ ██║██║██║ ██║██╔════╝██╔══██╗██║ ██╔═══██╗██╔════╝██║' ' ██║ ███╗█████╗ ██╔██╗ ██║██║██║ ██║███████╗╚█████╔╝██║ ██║ ██║██║ ██║' ' ██║ ██║██╔══╝ ██║╚██╗██║██║██║ ██║╚════██║██╔══██╗██║ ██║ ██║██║ ██║' ' ╚██████╔╝███████╗██║ ╚████║██║╚██████╔╝███████║╚█████╔╝███████╗╚██████╔╝╚██████╗██║' ' ╚═════╝ ╚══════╝╚═╝ ╚═══╝╚═╝ ╚═════╝ ╚══════╝ ╚════╝ ╚══════╝ ╚═════╝ ╚═════╝╚═╝' ) # --- Права администратора --- function Test-Admin { $id = [Security.Principal.WindowsIdentity]::GetCurrent() $pr = New-Object Security.Principal.WindowsPrincipal($id) return $pr.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } # Перезапуск самого себя через UAC. $true — повышенный процесс запущен, # текущий должен завершиться; $false — пользователь отказал. function Start-Elevated { $q = [string][char]34 $exe = Join-Path $PSHOME 'powershell.exe' $line = 'irm ' + [char]39 + $SelfUrl + [char]39 + ' | iex' $psArgs = '-NoProfile -ExecutionPolicy Bypass -Command ' + $q + $line + $q try { Start-Process -FilePath $exe -ArgumentList $psArgs -Verb RunAs -ErrorAction Stop return $true } catch { return $false } } # --- Размер консоли: окно 120x40, буфер с прокруткой для вывода скриптов --- function Set-ConsoleSize { param([int]$W = 120, [int]$WinH = 40, [int]$BufH = 3000) try { $ui = $Host.UI.RawUI $ui.WindowTitle = 'GLib - genius8loci' $ui.BackgroundColor = 'Black' $ui.ForegroundColor = 'Gray' $max = $ui.MaxPhysicalWindowSize $w = [Math]::Min($W, $max.Width) $h = [Math]::Min($WinH, $max.Height) $cur = $ui.WindowSize # Порядок обязателен: сжать окно -> задать буфер -> развернуть окно $ui.WindowSize = New-Object System.Management.Automation.Host.Size([Math]::Min($cur.Width, $w), [Math]::Min($cur.Height, $h)) $ui.BufferSize = New-Object System.Management.Automation.Host.Size($w, [Math]::Max($BufH, $h)) $ui.WindowSize = New-Object System.Management.Automation.Host.Size($w, $h) } catch { } } function Get-CmdGists { try { $all = Invoke-RestMethod -Uri $ApiUrl -ErrorAction Stop return @($all | Where-Object { $_.files.PSObject.Properties.Value.filename -match '\.(cmd|bat)$' }) } catch { Write-Host "[!] Ошибка получения списка: $_" -ForegroundColor $Colors.Error return @() } } # Описание и список .cmd/.bat одного гиста function Get-GistInfo { param($Gist) $d = $Gist.description if ($d -and $d.Trim()) { $desc = $d.Trim() } else { $desc = '<Без описания>' } $files = @($Gist.files.PSObject.Properties.Value | Where-Object { $_.filename -match '\.(cmd|bat)$' }) return [pscustomobject]@{ Desc = $desc; Files = $files; Names = ($files.filename -join ', ') } } # Колонка фиксированной ширины: обрезка с многоточием либо добивка пробелами function Format-Field { param([string]$Text, [int]$Width) if ($Text.Length -gt $Width) { return $Text.Substring(0, $Width - 3) + '...' } return $Text.PadRight($Width) } # Сохранение скрипта в вид, который понимает cmd.exe: # CRLF вместо LF (гисты с веб-интерфейса приходят с LF) + подходящая кодовая страница function Save-CmdFile { param([string]$Url, [string]$Path) $txt = [string](Invoke-RestMethod -Uri $Url -ErrorAction Stop) $cr = [string][char]13 $lf = [string][char]10 $crlf = $cr + $lf $txt = $txt.Replace($crlf, $lf).Replace($cr, $lf).Replace($lf, $crlf) # UTF-8 без BOM — только если скрипт сам переключается на 65001, иначе OEM (866) if ($txt -match 'chcp[ ]+65001') { $enc = New-Object System.Text.UTF8Encoding($false) } else { $enc = [System.Text.Encoding]::GetEncoding(866) } [System.IO.File]::WriteAllText($Path, $txt, $enc) } function Show-Banner { Clear-Host Write-Host '' foreach ($line in $Banner) { Write-Host $line -ForegroundColor $Colors.Title } Write-Host '' } # Перерисовка меню на месте, начиная со строки $Top (без мерцания Clear-Host) function Show-Menu { param([array]$Gists, [int]$Selected, [int]$Top) $ui = $Host.UI.RawUI $pad = $ui.BufferSize.Width - 1 $ui.CursorPosition = New-Object System.Management.Automation.Host.Coordinates(0, $Top) for ($i = 0; $i -lt $Gists.Count; $i++) { $info = Get-GistInfo $Gists[$i] $desc = Format-Field $info.Desc $DescWidth $names = Format-Field $info.Names $NameWidth if ($i -eq $Selected) { $head = ' > [{0}] ' -f ($i + 1).ToString('D2') } else { $head = ' [{0}] ' -f ($i + 1).ToString('D2') } $line = $head + $desc + ' — ' + $names if ($line.Length -gt $pad) { $line = $line.Substring(0, $pad) } if ($i -eq $Selected) { Write-Host $line.PadRight($pad) -ForegroundColor $Colors.SelFg -BackgroundColor $Colors.SelBg } else { Write-Host -NoNewline $head -ForegroundColor $Colors.Index Write-Host -NoNewline $desc -ForegroundColor $Colors.Menu Write-Host -NoNewline ' — ' -ForegroundColor $Colors.Menu Write-Host -NoNewline $names -ForegroundColor $Colors.FileName Write-Host (' ' * [Math]::Max(0, $pad - $line.Length)) } } Write-Host (' ' * $pad) $hint = ' ↑ / ↓ — выбор Enter — запуск Esc — выход' # -NoNewline на последней строке: не даём буферу прокрутиться Write-Host -NoNewline $hint.PadRight($pad) -ForegroundColor $Colors.Hint } # Запуск в текущем окне, без порождения второго cmd function Invoke-GistCmd { param($Gist) $enc = [Console]::OutputEncoding try { [Console]::CursorVisible = $true Clear-Host foreach ($f in (Get-GistInfo $Gist).Files) { $out = Join-Path $env:TEMP $f.filename Write-Host ("--- $($f.filename) ---") -ForegroundColor $Colors.Index Write-Host '' try { Save-CmdFile -Url $f.raw_url -Path $out & $env:ComSpec '/c' $out } catch { Write-Host "[!] Ошибка: $_" -ForegroundColor $Colors.Error } } } finally { # Скрипт мог сменить кодовую страницу через chcp — возвращаем свою try { [Console]::OutputEncoding = $enc } catch { } [Console]::CursorVisible = $false } Write-Host '' Write-Host '--- Завершено. Нажмите любую клавишу для возврата в меню ---' -ForegroundColor $Colors.Hint [void]$Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown') } function Start-GLib { # ReadKey доступен только в консольном хосте if ($Host.Name -ne 'ConsoleHost') { Write-Host '[!] Требуется консоль Windows. Запустите через powershell.exe.' -ForegroundColor $Colors.Error return } # Повышение прав: повторный запуск уже придёт сюда администратором if (-not (Test-Admin)) { Write-Host '[*] Запрос прав администратора...' -ForegroundColor $Colors.Hint if (Start-Elevated) { return } Write-Host '[!] Повышение прав отменено.' -ForegroundColor $Colors.Error Start-Sleep 3 return } Set-ConsoleSize $gists = Get-CmdGists Show-Banner if ($gists.Count -eq 0) { Write-Host ' Нет доступных .cmd-скриптов.' -ForegroundColor $Colors.Error Start-Sleep 3 return } $top = $Host.UI.RawUI.CursorPosition.Y $sel = 0 $vis = [Console]::CursorVisible try { [Console]::CursorVisible = $false while ($true) { Show-Menu -Gists $gists -Selected $sel -Top $top $k = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown') switch ($k.VirtualKeyCode) { 38 { if ($sel -gt 0) { $sel-- } else { $sel = $gists.Count - 1 } } # Up 40 { if ($sel -lt $gists.Count - 1) { $sel++ } else { $sel = 0 } } # Down 36 { $sel = 0 } # Home 35 { $sel = $gists.Count - 1 } # End 13 { # Enter Invoke-GistCmd $gists[$sel] Show-Banner $top = $Host.UI.RawUI.CursorPosition.Y [Console]::CursorVisible = $false } 27 { return } # Esc default { $c = $k.Character if ($c -eq '0') { return } if ($c -match '[1-9]') { $n = [int]"$c" if ($n -le $gists.Count) { $sel = $n - 1 } } } } } } finally { [Console]::CursorVisible = $vis Clear-Host } } Start-GLib