Private/Interactive/Write-TBMenuHeader.ps1
|
function Get-TBHeaderModel { <# .SYNOPSIS Builds the interactive header content model from module metadata. #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter()] [string]$Subtitle ) if (-not $script:TBHeaderModelCache) { $moduleVersion = '0.0.0' try { $manifestPath = Join-Path -Path $script:TBModuleRoot -ChildPath 'TenantBaseline.psd1' if (Test-Path -Path $manifestPath -PathType Leaf) { $manifest = Import-PowerShellDataFile -Path $manifestPath if ($manifest.ModuleVersion) { $moduleVersion = [string]$manifest.ModuleVersion } } } catch { # Keep default metadata values when manifest cannot be read. } $script:TBHeaderModelCache = [PSCustomObject]@{ Title = 'TenantBaseline' DefaultSubtitle = 'Unified Tenand Configuration Management' VersionText = ('Version: v{0}' -f $moduleVersion) AuthorText = 'Author: Ugur' WebsiteText = 'Website: tenantbaseline.com' LinkedInText = 'LinkedIn: linkedin.com/in/ugurkocde' RepositoryText = 'Repository: github.com/ugurkocde/tenantbaseline' UTCMText = 'UTCM: Microsoft Graph Unified Tenant Configuration Management For Cross-Workload Policy Governance.' BackendText = 'Backend: Uses The Microsoft First-Party UTCM App (App ID: 03b07b79-c5bc-4b5e-9bfa-13acf4a99998).' UseCasesText = 'Use Cases: Tenant Configuration Monitoring, Drift Detection, Snapshot Auditing.' FeaturesText = 'Features: Baseline Management, Monitor Workflows, Reports/Dashboard/Documentation, UTCM Setup Planning.' LinksLine = 'tenantbaseline.com | github.com/ugurkocde/tenantbaseline' UTCMShort = 'UTCM: Unified Tenant Configuration Management (Microsoft Graph)' CapabilitiesLine = 'Monitoring, Drift Detection, Snapshots, Baselines, Reports' } } $resolvedSubtitle = if ($Subtitle) { $Subtitle } else { $script:TBHeaderModelCache.DefaultSubtitle } return [PSCustomObject]@{ Title = $script:TBHeaderModelCache.Title Subtitle = $resolvedSubtitle VersionText = $script:TBHeaderModelCache.VersionText AuthorText = $script:TBHeaderModelCache.AuthorText WebsiteText = $script:TBHeaderModelCache.WebsiteText LinkedInText = $script:TBHeaderModelCache.LinkedInText RepositoryText = $script:TBHeaderModelCache.RepositoryText UTCMText = $script:TBHeaderModelCache.UTCMText BackendText = $script:TBHeaderModelCache.BackendText UseCasesText = $script:TBHeaderModelCache.UseCasesText FeaturesText = $script:TBHeaderModelCache.FeaturesText MetaLine = ('{0} | {1}' -f $script:TBHeaderModelCache.VersionText, $script:TBHeaderModelCache.AuthorText) LinksLine = $script:TBHeaderModelCache.LinksLine UTCMShort = $script:TBHeaderModelCache.UTCMShort CapabilitiesLine = $script:TBHeaderModelCache.CapabilitiesLine } } function Get-TBHeroLines { <# .SYNOPSIS Provides consistent hero content for premium and classic headers. .PARAMETER Premium When set, returns bold block-letter art for PS 7+ terminals. Otherwise returns clean ASCII art safe for all hosts. #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter(Mandatory = $true)] [pscustomobject]$HeaderModel, [Parameter()] [switch]$Premium ) # Unicode box-drawing and block character aliases $B = [string][char]0x2588 # Full block $TR = [string][char]0x2557 # Top-right double corner $TL = [string][char]0x2554 # Top-left double corner $BR = [string][char]0x255D # Bottom-right double corner $BL = [string][char]0x255A # Bottom-left double corner $H = [string][char]0x2550 # Horizontal double line $V = [string][char]0x2551 # Vertical double line if ($Premium) { # Big T at pos 1-9, big B at pos 22-29. $bGapWidth = 2 $bGap = ' ' * $bGapWidth # between T end and B start $sGap = ' ' * ($bGapWidth + 3) # stem is 3 chars narrower than top bar $artLines = @( ($B*8 + $TR + $bGap + $B*6 + $TR) ($BL + $H*2 + $B*2 + $TL + $H*2 + $BR + $bGap + $B*2 + $TL + $H*2 + $B*2 + $TR) (' ' + $B*2 + $V + $sGap + $B*6 + $TL + $BR) (' ' + $B*2 + $V + $sGap + $B*2 + $TL + $H*2 + $B*2 + $TR) (' ' + $B*2 + $V + $sGap + $B*6 + $TL + $BR) (' ' + $BL + $H + $BR + $sGap + $BL + $H*5 + $BR) ) $subtitleLine = $HeaderModel.Subtitle } else { # Figlet-inspired T/B mark for plain terminals. # ASCII safe for all terminals (6 lines). $artLines = @( ' _______ ____ ' '|__ __| | _ \ ' ' | | | |_) | ' ' | | | _ < ' ' | | | |_) | ' ' |_| |____/ ' ) $subtitleLine = $HeaderModel.Subtitle } return [PSCustomObject]@{ ArtLines = $artLines SubtitleLine = $subtitleLine } } function Write-TBMenuHeader { <# .SYNOPSIS Displays the TenantBaseline interactive console banner. .DESCRIPTION Renders a premium box-drawn header with gradient title on PS 7+, or a plain ASCII border header in hosts without arrow-key support. .PARAMETER Subtitle Optional subtitle displayed below the main title. .PARAMETER Mode Header density mode: - Compact: title/subtitle only. - Rich: includes version, author, links, UTCM context, use-cases, and features. #> [CmdletBinding()] param( [Parameter()] [string]$Subtitle, [Parameter()] [ValidateSet('Compact', 'Rich')] [string]$Mode = 'Compact' ) $headerModel = Get-TBHeaderModel -Subtitle $Subtitle if (Test-TBArrowKeySupport) { Write-TBMenuHeaderPremium -HeaderModel $headerModel -Mode $Mode } else { Write-TBMenuHeaderClassic -HeaderModel $headerModel -Mode $Mode } } function Write-TBMenuHeaderPremium { <# .SYNOPSIS Premium box-drawn header with gradient colors for PS 7+. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [pscustomobject]$HeaderModel, [Parameter()] [ValidateSet('Compact', 'Rich')] [string]$Mode = 'Compact' ) $palette = Get-TBColorPalette $esc = [char]27 $reset = "${esc}[0m" $innerWidth = Get-TBConsoleInnerWidth $fitText = { param([string]$Text) if ($null -eq $Text) { return '' } if ($Text.Length -le $innerWidth) { return $Text } if ($innerWidth -le 3) { return $Text.Substring(0, [Math]::Max(0, $innerWidth)) } return $Text.Substring(0, $innerWidth - 3) + '...' } $writePaddedLine = { param( [string]$Text, [string]$Style ) $clipped = & $fitText $Text $padded = $clipped.PadRight($innerWidth) Write-Host (' {0}{1}{2}{3}{4}{5}' -f $palette.Surface, ([char]0x2502), $Style, $padded, $reset, ('{0}{1}{2}' -f $palette.Surface, ([char]0x2502), $reset)) } $hero = Get-TBHeroLines -HeaderModel $HeaderModel -Premium $subtitleText = & $fitText $hero.SubtitleLine $writeCenteredStyledLine = { param( [string]$Text, [string]$Style ) $clipped = & $fitText $Text $pad = $innerWidth - $clipped.Length $left = [Math]::Floor($pad / 2) $right = $pad - $left Write-Host (' {0}{1}{2}{3}{4}{5}{6}{7}' -f $palette.Surface, ([char]0x2502), (' ' * $left), $Style, $clipped, (' ' * $right), $palette.Surface, ([char]0x2502)) -NoNewline Write-Host $palette.Reset } # Color definitions for gradients $blueRGB = @(137, 180, 250) $mauveRGB = @(203, 166, 247) $dimRGB = @(108, 112, 134) $tealRGB = @(148, 226, 213) # Top border $topLine = Get-TBGradientLine -Character ([char]0x2500) -Length $innerWidth -StartRGB $blueRGB -EndRGB $mauveRGB Write-Host (' {0}{1}{2}{3}' -f $palette.Surface, ([char]0x256D), $topLine, ([char]0x256E)) -NoNewline Write-Host $palette.Reset # Empty line $emptyInner = ' ' * $innerWidth Write-Host (' {0}{1}{2}{3}' -f $palette.Surface, ([char]0x2502), $emptyInner, ([char]0x2502)) -NoNewline Write-Host $palette.Reset # ASCII art title (centered as a single block with gradient) $heroArtLines = @($hero.ArtLines | ForEach-Object { $_.TrimEnd() }) $heroArtWidth = ($heroArtLines | Measure-Object -Property Length -Maximum).Maximum foreach ($artLine in $heroArtLines) { $artNormalized = $artLine.PadRight($heroArtWidth) $artClipped = & $fitText $artNormalized $artPad = $innerWidth - $artClipped.Length $artLeft = [Math]::Floor($artPad / 2) $artRight = $artPad - $artLeft $gradientArt = Get-TBGradientString -Text $artClipped -StartRGB $blueRGB -EndRGB $mauveRGB -Prefix "${esc}[1m" Write-Host (' {0}{1}{2}{3}{4}{5}{6}' -f $palette.Surface, ([char]0x2502), (' ' * $artLeft), $gradientArt, (' ' * $artRight), $palette.Surface, ([char]0x2502)) -NoNewline Write-Host $palette.Reset } # Brand line under monogram $brandText = & $fitText $HeaderModel.Title $brandPad = $innerWidth - $brandText.Length $brandLeft = [Math]::Floor($brandPad / 2) $brandRight = $brandPad - $brandLeft $gradientBrand = Get-TBGradientString -Text $brandText -StartRGB $blueRGB -EndRGB $mauveRGB -Prefix "${esc}[1m" Write-Host (' {0}{1}{2}{3}{4}{5}{6}' -f $palette.Surface, ([char]0x2502), (' ' * $brandLeft), $gradientBrand, (' ' * $brandRight), $palette.Surface, ([char]0x2502)) -NoNewline Write-Host $palette.Reset # Subtitle line (centered, gradient dim to teal, italic) $subPad = $innerWidth - $subtitleText.Length $subLeft = [Math]::Floor($subPad / 2) $subRight = $subPad - $subLeft $gradientSub = Get-TBGradientString -Text $subtitleText -StartRGB $dimRGB -EndRGB $tealRGB -Prefix "${esc}[3m" Write-Host (' {0}{1}{2}{3}{4}{5}{6}' -f $palette.Surface, ([char]0x2502), (' ' * $subLeft), $gradientSub, (' ' * $subRight), $palette.Surface, ([char]0x2502)) -NoNewline Write-Host $palette.Reset # Connection status and identity lines try { $connStatus = Get-TBConnectionStatus if ($connStatus.Connected) { $identityLabel = Format-TBTenantIdentity -ConnectionStatus $connStatus $statusText = 'Status: Connected' $statusStyle = $palette.Green $identityText = 'Organization: {0}' -f $identityLabel $identityStyle = $palette.Teal $actionText = $null $actionStyle = $palette.Dim } else { $statusText = 'Status: Sign-in required' $statusStyle = $palette.Red $identityText = 'Organization: n/a' $identityStyle = $palette.Dim $actionText = 'Action: Select Sign in to continue' $actionStyle = $palette.Yellow } } catch { $statusText = 'Status: Unknown' $statusStyle = $palette.Yellow $identityText = 'Organization: n/a' $identityStyle = $palette.Dim $actionText = 'Action: Open Connection Status to sign in' $actionStyle = $palette.Yellow } & $writeCenteredStyledLine -Text $statusText -Style $statusStyle & $writeCenteredStyledLine -Text $identityText -Style $identityStyle if ($actionText) { & $writeCenteredStyledLine -Text $actionText -Style $actionStyle } if ($Mode -eq 'Rich') { & $writePaddedLine -Text '' -Style $palette.Text & $writePaddedLine -Text $HeaderModel.MetaLine -Style $palette.Dim & $writePaddedLine -Text $HeaderModel.LinksLine -Style $palette.Subtext & $writePaddedLine -Text $HeaderModel.UTCMShort -Style $palette.Peach & $writePaddedLine -Text $HeaderModel.CapabilitiesLine -Style $palette.Subtext } # Empty line Write-Host (' {0}{1}{2}{3}' -f $palette.Surface, ([char]0x2502), $emptyInner, ([char]0x2502)) -NoNewline Write-Host $palette.Reset # Separator $sepLine = Get-TBGradientLine -Character ([char]0x2500) -Length $innerWidth -StartRGB $blueRGB -EndRGB $mauveRGB Write-Host (' {0}{1}{2}{3}' -f $palette.Surface, ([char]0x251C), $sepLine, ([char]0x2524)) -NoNewline Write-Host $palette.Reset } function Write-TBMenuHeaderClassic { <# .SYNOPSIS Classic ASCII header for non-interactive hosts. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [pscustomobject]$HeaderModel, [Parameter()] [ValidateSet('Compact', 'Rich')] [string]$Mode = 'Compact' ) $maxWidth = [Math]::Min((Get-TBConsoleInnerWidth), 100) if ($maxWidth -lt 40) { $maxWidth = 40 } $fitText = { param([string]$Text) if ($null -eq $Text) { return '' } if ($Text.Length -le $maxWidth) { return $Text } if ($maxWidth -le 3) { return $Text.Substring(0, [Math]::Max(0, $maxWidth)) } return $Text.Substring(0, $maxWidth - 3) + '...' } $centerText = { param([string]$Text) $clipped = & $fitText $Text if ($clipped.Length -ge $maxWidth) { return $clipped } $left = [Math]::Floor(($maxWidth - $clipped.Length) / 2) $right = $maxWidth - $clipped.Length - $left return (' ' * $left) + $clipped + (' ' * $right) } $hero = Get-TBHeroLines -HeaderModel $HeaderModel $heroArtLines = @($hero.ArtLines | ForEach-Object { $_.TrimEnd() }) $heroArtWidth = ($heroArtLines | Measure-Object -Property Length -Maximum).Maximum $lines = [System.Collections.ArrayList]::new() foreach ($heroLine in $heroArtLines) { $null = $lines.Add((& $centerText $heroLine.PadRight($heroArtWidth))) } $null = $lines.Add((& $centerText $HeaderModel.Title)) $null = $lines.Add((& $centerText $hero.SubtitleLine)) # Connection status lines try { $connStatus = Get-TBConnectionStatus if ($connStatus.Connected) { $statusText = 'Status: Connected' $identityText = 'Organization: {0}' -f (Format-TBTenantIdentity -ConnectionStatus $connStatus) $actionText = $null } else { $statusText = 'Status: Sign-in required' $identityText = 'Organization: n/a' $actionText = 'Action: Select Sign in to continue' } } catch { $statusText = 'Status: Unknown' $identityText = 'Organization: n/a' $actionText = 'Action: Open Connection Status to sign in' } $null = $lines.Add((& $centerText $statusText)) $null = $lines.Add((& $centerText $identityText)) if ($actionText) { $null = $lines.Add((& $centerText $actionText)) } if ($Mode -eq 'Rich') { $null = $lines.Add((& $fitText $HeaderModel.MetaLine).PadRight($maxWidth)) $null = $lines.Add((& $fitText $HeaderModel.LinksLine).PadRight($maxWidth)) $null = $lines.Add((& $fitText $HeaderModel.UTCMShort).PadRight($maxWidth)) $null = $lines.Add((& $fitText $HeaderModel.CapabilitiesLine).PadRight($maxWidth)) } $border = '=' * ($maxWidth + 2) $shadowBorder = '-' * ($maxWidth + 2) Write-Host '' Write-Host (' {0}' -f $shadowBorder) -ForegroundColor DarkGray Write-Host (' {0}' -f $border) -ForegroundColor Cyan foreach ($line in $lines) { Write-Host (' |{0}|' -f $line) -ForegroundColor Cyan } Write-Host (' {0}' -f $border) -ForegroundColor Cyan Write-Host (' {0}' -f $shadowBorder) -ForegroundColor DarkGray Write-Host '' } |