src/UI/Screens/ConfigScreen.ps1
|
# ConfigScreen — Pantalla TUI de configuración global de repo-nav. # # Muestra el estado de la instalación (alias registrado, profile, deps) y # permite togglear settings runtime (AutoLoadMode). Se accede via: # rnav config — desde la línea de comandos (se invoca directo aquí) # En la TUI: pendiente de binding (probable: 'P' para Preferences). # # Decisiones de scope: # - Cambiar/quitar/agregar el alias NO se hace desde acá. El usuario lo hace # con `rnav init -AliasName <nuevo> -Force` o `rnav uninstall`. Razón: para # modificar el $PROFILE necesitamos mutar archivo del usuario, lo que abre # ventanas de error que es mejor manejar desde el flujo normal de install # con su own pre-flight, prompts y mensajes — no replicar acá. # - Las settings que sí se pueden tocar (AutoLoadMode, futuros toggles) van # por SettingsService directo, sin shell-out. class ConfigScreen { [object] $Theme [object] $Renderer [object] $Primitives [object] $Frame [object] $AppHeader [object] $StatusBar [object] $I18n = $null # Inyectable post-construct. [object] $SettingsSvc [object] $SetupSvc [object] $Settings # snapshot mutable; al salir se Save al SettingsSvc [int] $SelectedIndex = 0 [bool] $Dirty = $false # si se cambió algo y hay que persistir [bool] $QuitRequested = $false [bool] $UseAltBuffer = $true # Items navegables: AutoLoadMode (cycle), BackgroundFetchInterval (cycle), Volver. static [string[]] $AutoLoadModes = @('All', 'Favorites', 'None') # Valores en segundos. 0 = off; el resto, intervalos típicos. # Etiqueta paralela en BgFetchLabels para mostrar al usuario. static [int[]] $BgFetchIntervals = @(0, 300, 600, 1800) static [string[]] $BgFetchLabels = @('off', 'cada 5 min', 'cada 10 min', 'cada 30 min') ConfigScreen($theme, $renderer, $primitives, $frame, $appHeader, $statusBar, $settingsSvc, $setupSvc) { $this.Theme = $theme $this.Renderer = $renderer $this.Primitives = $primitives $this.Frame = $frame $this.AppHeader = $appHeader $this.StatusBar = $statusBar $this.SettingsSvc = $settingsSvc $this.SetupSvc = $setupSvc } [void] Open() { $this.Settings = $this.SettingsSvc.Load() $this.SelectedIndex = 0 $this.Dirty = $false 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') { break } if ($exit -eq 'quit') { $this.QuitRequested = $true; break } $this.Render($errOut) } } finally { if ($this.Dirty) { $this.SettingsSvc.Save($this.Settings) } } } hidden [void] Render([object]$errOut) { if ([Console]::IsInputRedirected) { return } $lines = $this.BuildLines() $payload = [AnsiService]::BeginSync() + [AnsiService]::MoveTo(1, 1) + ($lines -join "`e[K`n") + "`e[K`e[J" + [AnsiService]::EndSync() $errOut.Write($payload) $errOut.Flush() } hidden [string] HandleKey([System.ConsoleKeyInfo]$key) { $k = $key.Key $c = $key.KeyChar if ($c -eq 'q' -or $c -eq 'Q') { return 'quit' } if ($k -eq 'Escape' -or $k -eq 'LeftArrow') { return 'back' } # 3 items navegables: 0=AutoLoadMode, 1=BackgroundFetchInterval, 2=Volver. $maxIdx = 2 if ($k -eq 'UpArrow' -or $c -eq 'k') { $this.SelectedIndex = if ($this.SelectedIndex -le 0) { $maxIdx } else { $this.SelectedIndex - 1 } return '' } if ($k -eq 'DownArrow' -or $c -eq 'j') { $this.SelectedIndex = if ($this.SelectedIndex -ge $maxIdx) { 0 } else { $this.SelectedIndex + 1 } return '' } if ($k -eq 'Enter' -or $c -eq ' ') { switch ($this.SelectedIndex) { 0 { $this.CycleAutoLoadMode(); return '' } 1 { $this.CycleBgFetchInterval(); return '' } 2 { return 'back' } } } return '' } hidden [void] CycleBgFetchInterval() { $intervals = [ConfigScreen]::BgFetchIntervals $current = if ($null -ne $this.Settings.BackgroundFetchInterval) { [int]$this.Settings.BackgroundFetchInterval } else { 0 } $idx = [array]::IndexOf($intervals, $current) if ($idx -lt 0) { $idx = 0 } $next = $intervals[($idx + 1) % $intervals.Count] $this.Settings.BackgroundFetchInterval = $next $this.Dirty = $true } hidden [void] CycleAutoLoadMode() { $modes = [ConfigScreen]::AutoLoadModes $current = if ($this.Settings.AutoLoadMode) { $this.Settings.AutoLoadMode } else { 'All' } $idx = [array]::IndexOf($modes, $current) if ($idx -lt 0) { $idx = 0 } $next = $modes[($idx + 1) % $modes.Count] $this.Settings.AutoLoadMode = $next $this.Dirty = $true } [string[]] BuildLines() { $reset = [AnsiService]::Reset $r = $this.Renderer $t = $this.Theme $lines = [System.Collections.Generic.List[string]]::new() # Title bar — version se podría inyectar desde el caller, por ahora # estática 'v3' para no acoplar la screen al RNVersion del entry script. $lines.Add($this.Frame.TitleBar('repo-nav', 'Configuración', 'v3')) $lines.Add($this.AppHeader.Render(@(), 'Config', @())) $lines.Add($r.HRule()) # ─── Sección Instalación ──────────────────────────────────────────── $lines.Add('') $lines.Add(' ' + $t.Fg('fg2') + 'Instalación' + $reset) $lines.Add($r.HRule()) $aliases = $this.SetupSvc.GetInstalledAliases() if ($aliases.Count -gt 0) { $aliasStr = $aliases -join ', ' $lines.Add(' ' + $t.Fg('fg2') + 'Alias: ' + $reset + $t.Fg('gitClean') + $aliasStr + $reset) } else { $lines.Add(' ' + $t.Fg('fg2') + 'Alias: ' + $reset + $t.Fg('gitConflict') + '(no instalado)' + $reset) $lines.Add(' ' + $t.Fg('fg3') + ' Ejecutá: rnav init' + $reset) } $profileInfo = $this.SetupSvc.GetProfileInfo() $profilePath = if ($profileInfo.Path) { $profileInfo.Path } else { '(no definido)' } $lines.Add(' ' + $t.Fg('fg2') + 'Profile: ' + $reset + $t.Fg('fg1') + $profilePath + $reset) $lines.Add(' ' + $t.Fg('fg3') + 'Para cambiar el alias: rnav init -AliasName <nuevo> -Force' + $reset) $lines.Add(' ' + $t.Fg('fg3') + 'Para quitar: rnav uninstall' + $reset) # ─── Sección Dependencias ─────────────────────────────────────────── $lines.Add('') $lines.Add(' ' + $t.Fg('fg2') + 'Dependencias' + $reset) $lines.Add($r.HRule()) $deps = $this.SetupSvc.CheckDependencies() foreach ($name in @('PowerShell', 'Git', 'Node', 'Gh')) { $d = $deps[$name] $label = if ($name -eq 'Gh') { 'gh (GitHub CLI)' } else { $name.ToLower() } $labelPad = $label.PadRight(18) if ($d.Available) { $tick = $t.Fg('gitClean') + '✓' + $reset $verStr = $t.Fg('fg1') + $d.Version + $reset $lines.Add(' ' + $tick + ' ' + $t.Fg('fg2') + $labelPad + $reset + $verStr) } elseif ($d.Required) { $cross = $t.Fg('gitConflict') + '✗' + $reset $note = $t.Fg('gitConflict') + '(requerido) ' + $reset + $t.Fg('fg3') + $d.Notes + $reset $lines.Add(' ' + $cross + ' ' + $t.Fg('fg2') + $labelPad + $reset + $note) } else { $cross = $t.Fg('fg3') + '·' + $reset $note = $t.Fg('fg3') + '(opcional) ' + $d.Notes + $reset $lines.Add(' ' + $cross + ' ' + $t.Fg('fg2') + $labelPad + $reset + $note) } } # ─── Sección Settings (interactivos) ──────────────────────────────── $lines.Add('') $lines.Add(' ' + $t.Fg('fg2') + 'Preferencias' + $reset) $lines.Add($r.HRule()) # Item 0: AutoLoadMode (cycle) $modeNow = if ($this.Settings.AutoLoadMode) { $this.Settings.AutoLoadMode } else { 'All' } $modeDesc = switch ($modeNow) { 'All' { 'todos los repos cargan git status al arrancar' } 'Favorites' { 'solo los favoritos cargan; el resto se carga al pasar encima' } 'None' { 'ninguno carga; pulsá R o navegá para cargar' } default { '' } } $lines.Add($this.RenderItem(0, 'AutoLoadMode', "$modeNow ▶ ($modeDesc)")) # Item 1: BackgroundFetchInterval (cycle off / 5min / 10min / 30min) $bgInterval = if ($null -ne $this.Settings.BackgroundFetchInterval) { [int]$this.Settings.BackgroundFetchInterval } else { 0 } $bgIdx = [array]::IndexOf([ConfigScreen]::BgFetchIntervals, $bgInterval) if ($bgIdx -lt 0) { $bgIdx = 0 } $bgLabel = [ConfigScreen]::BgFetchLabels[$bgIdx] $bgDesc = if ($bgInterval -eq 0) { 'fetch sigue manual (binding R, pull, push). Recomendado para red estable.' } else { 'corre git fetch en background sin bloquear UI. Útil con red lenta.' } $lines.Add($this.RenderItem(1, 'BackgroundFetch', "$bgLabel ▶ ($bgDesc)")) # Item 2: Volver $lines.Add('') $lines.Add($this.RenderItem(2, '[ Volver ]', '')) # Hint de que hay cambios pendientes if ($this.Dirty) { $lines.Add('') $lines.Add(' ' + $t.Fg('gitDirty') + '· Cambios pendientes — se guardan al volver con ↵ o Esc.' + $reset) } # StatusBar $tHelper = if ($null -ne $this.I18n) { $this.I18n } else { [I18nService]::new('es') } $hints = @( @{ k = '↑↓'; label = $tHelper.T('hint.move') } @{ k = '↵'; label = $tHelper.T('hint.toggle') } @{ k = 'Esc'; label = $tHelper.T('hint.back') } @{ k = 'Q'; label = $tHelper.T('hint.quit') } ) $lines.Add($r.HRule()) return @($lines.ToArray()) + $this.StatusBar.RenderAnchored($hints, '', $lines.Count) } hidden [string] RenderItem([int]$idx, [string]$label, [string]$value) { $reset = [AnsiService]::Reset $sel = ($this.SelectedIndex -eq $idx) $marker = if ($sel) { $this.Theme.Fg('acc') + '▎ ▶ ' + $reset } else { ' · ' } $labelFmt = [Renderer]::PadRight($this.Theme.Fg($(if ($sel) { 'acc' } else { 'fg2' })) + $label + $reset, 22) $valFmt = if ($value) { ' ' + $this.Theme.Fg('fg1') + $value + $reset } else { '' } return $marker + $labelFmt + $valFmt } } |