src/UI/Screens/GraphScreen.ps1
|
# GraphScreen — Render del git log --graph con scroll H/V (réplica de # BranchGraphView del v2). # # Read-only — solo muestra. Sin acciones que muten estado. # Bindings: # ↑↓ / jk scroll 1 línea vertical # PgUp/PgDn scroll página # g / Home ir al inicio # G / End ir al final # ←→ / hl scroll horizontal 8 chars # ESC / q volver class GraphScreen { [object] $Theme [object] $Renderer [object] $Primitives [object] $Frame [object] $AppHeader [object] $StatusBar [object] $Git [object] $I18n = $null # Inyectable post-construct. [object] $Repo [string[]] $Lines = @() [int] $ScrollY = 0 [int] $ScrollX = 0 [int] $HScrollStep = 8 [bool] $QuitRequested = $false [bool] $UseAltBuffer = $true [string] $StatusMessage = '' [string] $StatusKind = '' GraphScreen($theme, $renderer, $primitives, $frame, $appHeader, $statusBar, $git) { $this.Theme = $theme $this.Renderer = $renderer $this.Primitives = $primitives $this.Frame = $frame $this.AppHeader = $appHeader $this.StatusBar = $statusBar $this.Git = $git } [void] Open([object]$repo) { if ($null -eq $repo) { throw "GraphScreen.Open: repo no puede ser null." } $this.Repo = $repo $this.ScrollY = 0 $this.ScrollX = 0 $this.StatusMessage = '' $this.Lines = @() if ([Console]::IsInputRedirected) { $this.Lines = @($this.Git.GetGraph($repo.Path, 200)) $rendered = $this.BuildLines() foreach ($l in $rendered) { Write-Host $l } return } # Pre-frame con 'cargando…' antes del git call (que puede tomar 100-300ms # en repos con history grande). $errOut = [Console]::Error $this.SetStatus('cargando graph…', 'ok') $this.Render($errOut) # Carga real $this.Lines = @($this.Git.GetGraph($repo.Path, 200)) if ($this.Lines.Count -eq 0) { $this.SetStatus('no hay commits o el path no es git', 'error') } else { $this.SetStatus('', '') } $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 } # `$rendered` en lugar de `$lines`/`$frame` — chocan con properties. $rendered = $this.BuildLines() $framePayload = [AnsiService]::BeginSync() + [AnsiService]::MoveTo(1, 1) + ($rendered -join "`e[K`n") + "`e[K`e[J" + [AnsiService]::EndSync() $errOut.Write($framePayload) $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 $c -eq 'h' -and $false) { return 'back' } # h queda para horizontal scroll if ($k -eq 'Escape') { return 'back' } $contentH = $this.ContentHeight() $maxY = [Math]::Max(0, $this.Lines.Count - $contentH) $maxX = $this.MaxScrollX() if ($k -eq 'UpArrow' -or $c -eq 'k') { $this.ScrollY = [Math]::Max(0, $this.ScrollY - 1); return '' } if ($k -eq 'DownArrow' -or $c -eq 'j') { $this.ScrollY = [Math]::Min($maxY, $this.ScrollY + 1); return '' } if ($k -eq 'PageUp') { $this.ScrollY = [Math]::Max(0, $this.ScrollY - ($contentH - 1)); return '' } if ($k -eq 'PageDown') { $this.ScrollY = [Math]::Min($maxY, $this.ScrollY + ($contentH - 1)); return '' } if ($k -eq 'Home' -or $c -eq 'g') { $this.ScrollY = 0; return '' } if ($k -eq 'End' -or $c -eq 'G') { $this.ScrollY = $maxY; return '' } if ($k -eq 'LeftArrow') { $this.ScrollX = [Math]::Max(0, $this.ScrollX - $this.HScrollStep); return '' } if ($k -eq 'RightArrow') { $this.ScrollX = [Math]::Min($maxX, $this.ScrollX + $this.HScrollStep); return '' } return '' } hidden [void] SetStatus([string]$msg, [string]$kind) { $this.StatusMessage = $msg $this.StatusKind = $kind } # Cuántas líneas de contenido caben. Chrome = title + appheader + hr + status # + sectionTitle + hr + 2hr (statusbar 2 líneas) = 8. hidden [int] ContentHeight() { return [Math]::Max(3, [Console]::WindowHeight - 8) } # Longitud máxima de línea para clamp del scroll horizontal. hidden [int] MaxScrollX() { $maxLen = 0 foreach ($l in $this.Lines) { if ($l.Length -gt $maxLen) { $maxLen = $l.Length } } $width = $this.Renderer.Width() return [Math]::Max(0, $maxLen - $width + 4) } [string[]] BuildLines() { $reset = [AnsiService]::Reset $r = $this.Renderer # `$out` en lugar de `$lines` — choca con property $this.Lines (PS class CI). $out = [System.Collections.Generic.List[string]]::new() # Title $repoName = if ($this.Repo) { $this.Repo.Name } else { '—' } $out.Add($this.Frame.TitleBar('repo-nav', "Graph · $repoName", '1.0.0')) # AppHeader $bc = [BreadcrumbBuilder]::Build($this.Repo.Path) $segs = @($bc.Segs) + @($bc.Current) + @('Branches') $out.Add($this.AppHeader.Render($segs, 'Graph', @())) $out.Add($r.HRule()) # Status / prompt line if ($this.StatusMessage) { $color = if ($this.StatusKind -eq 'error') { 'gitConflict' } else { 'gitClean' } $out.Add(' ' + $this.Theme.Fg($color) + $this.StatusMessage + $reset) } else { $out.Add('') } # Section title con info de scroll $totalLines = $this.Lines.Count $contentH = $this.ContentHeight() $titleText = ' Commits · ' + $totalLines if ($totalLines -gt $contentH) { $start = $this.ScrollY + 1 $end = [Math]::Min($totalLines, $this.ScrollY + $contentH) $titleText += " · viendo $start–$end" } if ($this.ScrollX -gt 0) { $titleText += " · ←$($this.ScrollX) chars" } $out.Add($this.Theme.Fg('fg2') + $titleText + $reset) $out.Add($r.HRule()) # Slice vertical + horizontal $sliceStart = $this.ScrollY $sliceEnd = [Math]::Min($totalLines, $this.ScrollY + $contentH) if ($totalLines -eq 0) { $out.Add($this.Theme.Fg('fg3') + ' (sin commits)' + $reset) } else { $width = $r.Width() $contentWidth = [Math]::Max(10, $width - 2) for ($i = $sliceStart; $i -lt $sliceEnd; $i++) { $line = $this.Lines[$i] # Slice horizontal con safe substring $line = if ($this.ScrollX -lt $line.Length) { $line.Substring($this.ScrollX) } else { '' } # Trunca al ancho disponible para evitar wrap if ($line.Length -gt $contentWidth) { $line = $line.Substring(0, $contentWidth) } $out.Add(' ' + $this.Theme.Fg('fg1') + $line + $reset) } } # StatusBar $tHelper = if ($null -ne $this.I18n) { $this.I18n } else { [I18nService]::new('es') } $hints = @( @{ k = '↑↓'; label = 'Scroll' } @{ k = '←→'; label = 'Horiz' } @{ k = 'PgUp/PgDn'; label = 'Page' } @{ k = 'g/G'; label = 'Top/End' } @{ k = 'Esc'; label = $tHelper.T('hint.back') } @{ k = 'Q'; label = $tHelper.T('hint.quit') } ) $out.Add($r.HRule()) return @($out.ToArray()) + $this.StatusBar.RenderAnchored($hints, '', $out.Count) } } |