src/UI/Screens/SetupScreen.ps1
|
# SetupScreen — Vista de status del entorno + acciones de install/uninstall. # # Read-only mostly: muestra deps, paths, aliases instalados. 2 acciones: # Reinstall (Install.ps1 -Force) y Uninstall (Install.ps1 -Uninstall). Las # acciones invocan el script externo en proceso separado para no contaminar # el state actual. # # Accesible desde PreferencesScreen como un item 'Setup & Status'. class SetupScreen { [object] $Theme [object] $Renderer [object] $Primitives [object] $Frame [object] $AppHeader [object] $StatusBar [object] $Setup # SetupService [object] $I18n = $null # Inyectable post-construct. [int] $SelectedIndex = 0 [bool] $QuitRequested = $false [bool] $UseAltBuffer = $true [string] $StatusMessage = '' [string] $StatusKind = '' SetupScreen($theme, $renderer, $primitives, $frame, $appHeader, $statusBar, $setupSvc) { $this.Theme = $theme $this.Renderer = $renderer $this.Primitives = $primitives $this.Frame = $frame $this.AppHeader = $appHeader $this.StatusBar = $statusBar $this.Setup = $setupSvc } [void] Open() { $this.SelectedIndex = 0 $this.StatusMessage = '' $this.StatusKind = '' if ([Console]::IsInputRedirected) { $lines = $this.BuildLines() foreach ($l in $lines) { Write-Host $l } return } $errOut = [Console]::Error $this.Render($errOut) try { while ($true) { $key = [Console]::ReadKey($true) $exit = $this.HandleKey($key) if ($exit -eq 'back') { return } if ($exit -eq 'quit') { $this.QuitRequested = $true; return } $this.Render($errOut) } } finally { } } hidden [void] Render([object]$errOut) { if ([Console]::IsInputRedirected) { return } $lines = $this.BuildLines() $framePayload = [AnsiService]::BeginSync() + [AnsiService]::MoveTo(1, 1) + ($lines -join "`e[K`n") + "`e[K`e[J" + [AnsiService]::EndSync() $errOut.Write($framePayload) $errOut.Flush() } # Helper i18n con fallback graceful (mismo patrón que MainScreen.T). hidden [string] T([string]$key) { if ($null -ne $this.I18n) { return $this.I18n.T($key) } return ([I18nService]::new('es')).T($key) } hidden [string] T([string]$key, [object[]]$values) { if ($null -ne $this.I18n) { return $this.I18n.T($key, $values) } return ([I18nService]::new('es')).T($key, $values) } # Items: solo las acciones son seleccionables; las rows de info son display. # Las acciones tienen Action='reinstall'|'uninstall'|'cancel'. El nombre del # alias se resuelve dinámicamente — usa el primer alias managed instalado, # o 'rnav' (default oficial) si no hay ninguno. [object[]] Actions() { $installed = $this.Setup.GetInstalledAliases() $aliasName = if ($installed.Count -gt 0) { $installed[0] } else { 'rnav' } return @( @{ Label = $this.T('setup.reinstallAlias', @($aliasName)); Action = 'reinstall' } @{ Label = $this.T('setup.uninstallAlias', @($aliasName)); Action = 'uninstall' } @{ Label = $this.T('setup.cancel'); Action = 'cancel' } ) } hidden [string] HandleKey([System.ConsoleKeyInfo]$key) { $k = $key.Key $c = $key.KeyChar $actions = $this.Actions() if ($c -eq 'q' -or $c -eq 'Q') { return 'quit' } if ($k -eq 'Escape' -or $k -eq 'LeftArrow' -or $c -eq 'h') { return 'back' } if ($k -eq 'UpArrow' -or $c -eq 'k') { if ($actions.Count -gt 0) { $this.SelectedIndex = if ($this.SelectedIndex -le 0) { $actions.Count - 1 } else { $this.SelectedIndex - 1 } } return '' } if ($k -eq 'DownArrow' -or $c -eq 'j') { if ($actions.Count -gt 0) { $this.SelectedIndex = if ($this.SelectedIndex -ge $actions.Count - 1) { 0 } else { $this.SelectedIndex + 1 } } return '' } if ($k -eq 'Enter') { if ($actions.Count -eq 0) { return '' } $action = $actions[$this.SelectedIndex].Action switch ($action) { 'reinstall' { $this.RunInstall($true); return '' } 'uninstall' { $this.RunInstall($false); return '' } 'cancel' { return 'back' } } } return '' } hidden [void] RunInstall([bool]$install) { $repoRoot = $this.Setup.GetRepoRoot() $installScript = Join-Path $repoRoot 'Install.ps1' if (-not [System.IO.File]::Exists($installScript)) { $this.SetStatus($this.T('setup.installNotFound', @($repoRoot)), 'error') return } # Invocamos en proceso separado para evitar contaminar el state actual # de la sesión PS interactiva. $args = if ($install) { @('-Force') } else { @('-Uninstall') } $pwshExe = (Get-Command pwsh -ErrorAction SilentlyContinue).Source if (-not $pwshExe) { $pwshExe = 'pwsh' } try { $output = & $pwshExe -NoProfile -File $installScript @args 2>&1 $exitCode = $LASTEXITCODE if ($exitCode -eq 0) { $msgKey = if ($install) { 'setup.aliasReinstalled' } else { 'setup.aliasUninstalled' } $this.SetStatus($this.T($msgKey), 'ok') } else { $msg = ($output | Select-Object -Last 1).ToString() $this.SetStatus($this.T('setup.installFailed', @($exitCode, $msg)), 'error') } } catch { $this.SetStatus($this.T('setup.error', @($_.Exception.Message)), 'error') } } hidden [void] SetStatus([string]$msg, [string]$kind) { $this.StatusMessage = $msg $this.StatusKind = $kind } [string[]] BuildLines() { $reset = [AnsiService]::Reset $r = $this.Renderer $lines = [System.Collections.Generic.List[string]]::new() $title = $this.T('setup.title') # Title bar $lines.Add($this.Frame.TitleBar('repo-nav', $title, '1.0.0')) # AppHeader $lines.Add($this.AppHeader.Render(@($this.T('prefs.title')), $title, @())) $lines.Add($r.HRule()) # Status / prompt line if ($this.StatusMessage) { $color = if ($this.StatusKind -eq 'error') { 'gitConflict' } else { 'gitClean' } $lines.Add(' ' + $this.Theme.Fg($color) + $this.StatusMessage + $reset) } else { $lines.Add('') } # ─── Información del entorno ──────────────────────────────────────── $lines.Add($this.Theme.Fg('fg2') + ' ' + $this.T('setup.envSection') + $reset) $psVer = $this.Setup.GetPsVersion() $psOk = $false if ($psVer -and $psVer -ne 'unknown' -and $null -ne $global:PSVersionTable) { $psOk = $global:PSVersionTable.PSVersion.Major -ge 7 } $lines.Add($this.RenderInfoRow($this.T('setup.psLabel'), $psVer, $psOk)) $gitVer = $this.Setup.GetGitVersion() $gitDisplay = if ($gitVer) { $gitVer } else { $this.T('setup.gitNotInstalled') } $lines.Add($this.RenderInfoRow($this.T('setup.gitLabel'), $gitDisplay, [bool]$gitVer)) $repoRoot = $this.Setup.GetRepoRoot() $lines.Add($this.RenderInfoRow($this.T('setup.repoPath'), $repoRoot, $true)) # ─── Profile + aliases instalados ─────────────────────────────────── $lines.Add('') $lines.Add($this.Theme.Fg('fg2') + ' ' + $this.T('setup.profileSection') + $reset) $profInfo = $this.Setup.GetProfileInfo() $lines.Add($this.RenderInfoRow($this.T('setup.path'), $profInfo.Path, $profInfo.Exists)) # Mostramos los aliases registrados en el bloque managed. Soporta # cualquier nombre custom (rnav, listc, etc.) y combinaciones múltiples. $installedAliases = $this.Setup.GetInstalledAliases() $aliasOk = $installedAliases.Count -gt 0 $aliasMsg = if ($aliasOk) { $this.T('setup.aliasManaged', @(($installedAliases -join ', '))) } else { $this.T('setup.notInstalled') } $lines.Add($this.RenderInfoRow($this.T('setup.alias'), $aliasMsg, $aliasOk)) $functions = $this.Setup.GetInstalledFunctions() $userFunctions = @($functions | Where-Object { $_.Source -eq 'user' }) if ($userFunctions.Count -gt 0) { $names = ($userFunctions | ForEach-Object Name) -join ', ' $lines.Add($this.RenderInfoRow($this.T('setup.otherFunctions'), $names, $true)) } # ─── Settings ─────────────────────────────────────────────────────── $lines.Add('') $lines.Add($this.Theme.Fg('fg2') + ' ' + $this.T('setup.settingsSection') + $reset) $settingsInfo = $this.Setup.GetSettingsInfo() $lines.Add($this.RenderInfoRow($this.T('setup.path'), $settingsInfo.Path, $settingsInfo.Exists)) if ($settingsInfo.Exists) { $statusText = if ($settingsInfo.Valid) { $this.T('setup.jsonValid') } else { $this.T('setup.jsonInvalid') } $lines.Add($this.RenderInfoRow($this.T('setup.statusLabel'), $statusText, $settingsInfo.Valid)) } # ─── Acciones ─────────────────────────────────────────────────────── $lines.Add('') $lines.Add($r.HRule()) $lines.Add($this.Theme.Fg('fg2') + ' ' + $this.T('setup.actionsSection') + $reset) $actions = $this.Actions() for ($i = 0; $i -lt $actions.Count; $i++) { $sel = ($i -eq $this.SelectedIndex) $marker = if ($sel) { $this.Theme.Fg('acc') + '▎ ▶ ' + $reset } else { ' · ' } $color = switch ($actions[$i].Action) { 'uninstall' { 'gitDirty' } # naranja para acción "delete"-ish 'cancel' { 'fg2' } default { if ($sel) { 'acc' } else { 'fg1' } } } $lines.Add($marker + $this.Theme.Fg($color) + $actions[$i].Label + $reset) } # StatusBar $hints = @( @{ k = '↑↓'; label = $this.T('hint.move') } @{ k = '↵'; label = $this.T('hint.run') } @{ k = '←'; label = $this.T('hint.back') } @{ k = 'Q'; label = $this.T('hint.quit') } ) $lines.Add($r.HRule()) return @($lines.ToArray()) + $this.StatusBar.RenderAnchored($hints, '', $lines.Count) } hidden [string] RenderInfoRow([string]$label, [string]$value, [bool]$ok) { $reset = [AnsiService]::Reset $tickFg = if ($ok) { 'gitClean' } else { 'gitConflict' } $tick = $this.Theme.Fg($tickFg) + $(if ($ok) { '✓' } else { '✗' }) + $reset $labelFmt = [Renderer]::PadRight(' ' + $this.Theme.Fg('fg2') + $label + $reset, 22) $valFmt = $this.Theme.Fg('fg1') + $value + $reset return $tick + ' ' + $labelFmt + ' ' + $valFmt } } |