src/UI/Renderer.ps1
|
# Renderer — Helpers de bajo nivel para escribir al host. # Wrapper sobre AnsiService + utilidades de medición y padding. class Renderer { [object] $Theme # ThemeService (untyped por ps-class-scope) Renderer($themeService) { $this.Theme = $themeService } # Ancho visible del host. Override con env $RNCOLUMNS para testing/debug. # Fallback 120 si no se puede detectar. [int] Width() { $override = [Environment]::GetEnvironmentVariable('RNCOLUMNS') if ($override) { $n = 0 if ([int]::TryParse($override, [ref]$n) -and $n -gt 0) { return $n } } try { $w = $global:Host.UI.RawUI.WindowSize.Width if ($w -gt 0) { return $w } } catch { # Algunos hosts (CI sin tty, redirected stdin) no exponen RawUI — usamos default. $null = $_ } return 120 } [int] Height() { try { $h = $global:Host.UI.RawUI.WindowSize.Height if ($h -gt 0) { return $h } } catch { $null = $_ } return 38 } # Largo VISIBLE de un string (descarta secuencias ANSI ESC[…m / ESC[…H, etc.) static [int] VisibleLength([string]$s) { if ([string]::IsNullOrEmpty($s)) { return 0 } # Strip ESC[ ... letter (CSI sequences) $stripped = $s -replace "`e\[[0-9;?]*[a-zA-Z]", '' return $stripped.Length } # Trunca preservando ANSI escapes. Cuenta sólo chars visibles. static [string] TruncateAnsi([string]$s, [int]$visibleMax) { if ([string]::IsNullOrEmpty($s) -or $visibleMax -le 0) { return '' } $sb = [System.Text.StringBuilder]::new() $i = 0 $visible = 0 $len = $s.Length while ($i -lt $len -and $visible -lt $visibleMax) { $c = $s[$i] if ($c -eq [char]27 -and ($i + 1) -lt $len -and $s[$i + 1] -eq '[') { # CSI: ESC [ params letter — copiar entero, no cuenta como visible $j = $i + 2 while ($j -lt $len -and -not [char]::IsLetter($s[$j])) { $j++ } if ($j -lt $len) { $j++ } [void]$sb.Append($s.Substring($i, $j - $i)) $i = $j } else { [void]$sb.Append($c) $visible++ $i++ } } return $sb.ToString() } # Padding a derecha hasta $width (visibles). Truncate ANSI-safe con ellipsis si excede. # Garantiza ancho exacto y emite reset al final para que el siguiente token no herede color. static [string] PadRight([string]$s, [int]$width) { $vlen = [Renderer]::VisibleLength($s) if ($vlen -le $width) { return $s + [AnsiService]::Reset + (' ' * ($width - $vlen)) } $room = [Math]::Max(0, $width - 1) # 1 char para … $truncated = [Renderer]::TruncateAnsi($s, $room) $reset = [AnsiService]::Reset $vNow = [Renderer]::VisibleLength($truncated) + 1 # + ellipsis $pad = if ($vNow -lt $width) { ' ' * ($width - $vNow) } else { '' } return $truncated + '…' + $reset + $pad } static [string] PadLeft([string]$s, [int]$width) { $vlen = [Renderer]::VisibleLength($s) if ($vlen -le $width) { return (' ' * ($width - $vlen)) + $s + [AnsiService]::Reset } # Trunca por izquierda preservando escapes — uso TruncateAnsi (que trunca por derecha) # como fallback razonable. $truncated = [Renderer]::TruncateAnsi($s, [Math]::Max(0, $width - 1)) return $truncated + '…' + [AnsiService]::Reset } # Humaniza un delta de tiempo: "hace 5 s", "hace 12 min", "hace 3 h", # "hace 2 d", "nunca". Pensado para hints sutiles de "fetch hace X" en # filas de la lista. Mantiene los strings cortos para no romper layout. static [string] HumanizeAgo([object]$utcDateTime) { if ($null -eq $utcDateTime) { return 'nunca' } $now = [DateTime]::UtcNow $delta = $now - [DateTime]$utcDateTime $totalSec = [int]$delta.TotalSeconds if ($totalSec -lt 0) { return 'recién' } if ($totalSec -lt 60) { return 'hace ' + $totalSec + ' s' } $totalMin = [int][Math]::Floor($delta.TotalMinutes) if ($totalMin -lt 60) { return 'hace ' + $totalMin + ' min' } $totalHr = [int][Math]::Floor($delta.TotalHours) if ($totalHr -lt 24) { return 'hace ' + $totalHr + ' h' } $totalDay = [int][Math]::Floor($delta.TotalDays) if ($totalDay -lt 30) { return 'hace ' + $totalDay + ' d' } $totalMonth = [int][Math]::Floor($delta.TotalDays / 30) if ($totalMonth -lt 12) { return 'hace ' + $totalMonth + ' mes' } $totalYear = [int][Math]::Floor($delta.TotalDays / 365) return 'hace ' + $totalYear + ' año' } # Horizontal rule del ancho actual con color line. [string] HRule() { $color = $this.Theme.Fg('line') $reset = [AnsiService]::Reset return $color + ('─' * $this.Width()) + $reset } [string] HRuleStrong() { $color = $this.Theme.Fg('lineStrong') $reset = [AnsiService]::Reset return $color + ('─' * $this.Width()) + $reset } # Helper: aplicar color FG a un texto y resetear. [string] Colored([string]$themeKey, [string]$text) { return $this.Theme.Fg($themeKey) + $text + [AnsiService]::Reset } # Escribe líneas al host (preserva ANSI escapes). [void] Write([string]$line) { Write-Host $line } } |