Private/Console/Ansi.psm1
|
using namespace System using namespace System.IO using namespace System.Collections.Generic using namespace System.Text using module ..\Enums.psm1 using module ..\Abstracts.psm1 using module .\Colors.psm1 using module .\Internal.psm1 using module .\Rendering.psm1 class AnsiCapabilities { [ColorSystem]$ColorSystem [bool]$Ansi [bool]$Links [bool]$AlternateBuffer AnsiCapabilities() { $this.ColorSystem = [ColorSystem]::NoColors $this.Ansi = $false $this.Links = $false $this.AlternateBuffer = $false } } class AnsiCodeBuilder { static [byte[]] Build([Decoration]$decoration) { $codes = [List[byte]]::new() if (($decoration -band [Decoration]::Bold) -ne 0) { $codes.Add(1) } if (($decoration -band [Decoration]::Dim) -ne 0) { $codes.Add(2) } if (($decoration -band [Decoration]::Italic) -ne 0) { $codes.Add(3) } if (($decoration -band [Decoration]::Underline) -ne 0) { $codes.Add(4) } if (($decoration -band [Decoration]::SlowBlink) -ne 0) { $codes.Add(5) } if (($decoration -band [Decoration]::RapidBlink) -ne 0) { $codes.Add(6) } if (($decoration -band [Decoration]::Invert) -ne 0) { $codes.Add(7) } if (($decoration -band [Decoration]::Conceal) -ne 0) { $codes.Add(8) } if (($decoration -band [Decoration]::Strikethrough) -ne 0) { $codes.Add(9) } return $codes.ToArray() } static [byte[]] Build([ColorSystem]$system, [object]$color, [bool]$foreground) { if ($null -eq $color -or ([Color]$color).IsDefault) { return [byte[]]@() } $c = [Color]$color switch ($system) { ([ColorSystem]::NoColors) { return [byte[]]@() } ([ColorSystem]::TrueColor) { if ($null -ne $c.Number) { return [AnsiCodeBuilder]::GetEightBit($c, $foreground) } $mod = $foreground ? 38 : 48 return [byte[]]@($mod, 2, $c.R, $c.G, $c.B) } ([ColorSystem]::EightBit) { return [AnsiCodeBuilder]::GetEightBit($c, $foreground) } ([ColorSystem]::Standard) { return [AnsiCodeBuilder]::GetFourBit($c, $foreground) } ([ColorSystem]::Legacy) { return [AnsiCodeBuilder]::GetThreeBit($c, $foreground) } default { return [byte[]]@() } } return [byte[]]@() } static hidden [byte[]] GetThreeBit([Color]$color, [bool]$foreground) { $num = $color.Number if ($null -eq $num -or $num -ge 8) { $num = $color.ExactOrClosest([ColorSystem]::Legacy).Number } $mod = $foreground ? 30 : 40 return [byte[]]@([byte]($num + $mod)) } static hidden [byte[]] GetFourBit([Color]$color, [bool]$foreground) { $num = $color.Number if ($null -eq $num -or $num -ge 16) { $num = $color.ExactOrClosest([ColorSystem]::Standard).Number } $mod = $num -lt 8 ? ($foreground ? 30 : 40) : ($foreground ? 82 : 92) return [byte[]]@([byte]($num + $mod)) } static hidden [byte[]] GetEightBit([Color]$color, [bool]$foreground) { $num = $color.Number if ($null -eq $num) { $num = $color.ExactOrClosest([ColorSystem]::EightBit).Number } $mod = $foreground ? [byte]38 : [byte]48 return [byte[]]@($mod, 5, [byte]$num) } static [string] GetAnsi([Style]$style, [ColorSystem]$system) { $codes = [List[byte]]::new() $codes.AddRange([AnsiCodeBuilder]::Build($style.Decoration)) $codes.AddRange([AnsiCodeBuilder]::Build($system, $style.Foreground, $true)) $codes.AddRange([AnsiCodeBuilder]::Build($system, $style.Background, $false)) return [string]::Join(';', $codes) } } class AnsiWriter { hidden [ConsoleWriter]$_output hidden [int]$_linkCount [AnsiCapabilities]$Capabilities AnsiWriter() { $this.initialize([ConsoleWriter]::new()) } AnsiWriter([ConsoleWriter]$output) { $this.initialize($output) } hidden [void] initialize ([ConsoleWriter]$output) { $this._output = $output $this._output.WriteRaw = $true $this.Capabilities = [AnsiCapabilities]::new() $this.Capabilities.Ansi = $true $this.Capabilities.ColorSystem = [ColorSystem]::TrueColor $this.Capabilities.Links = $true $this._linkCount = 0 } [ConsoleWriter] GetOutput() { return $this._output } [void] Write([string]$text) { $this._output.Write($text) } [void] WriteLine() { $this._output.Write("`n") } [void] WriteLine([string]$text) { $this._output.Write($text + "`n") } [void] Write([string]$text, [Style]$style) { $this.Write($text, $style, $null) } [void] Write([string]$text, [Style]$style, [AnsiLink]$link) { $shouldClose = $false if ($this.Capabilities.Ansi) { if ($null -ne $link) { $this.BeginLink($link) } $codes = [List[byte]]::new() $codes.AddRange([AnsiCodeBuilder]::Build($style.Decoration)) $codes.AddRange([AnsiCodeBuilder]::Build($this.Capabilities.ColorSystem, $style.Foreground, $true)) $codes.AddRange([AnsiCodeBuilder]::Build($this.Capabilities.ColorSystem, $style.Background, $false)) if ($codes.Count -gt 0) { $this.WriteCsi([string]::Join(';', $codes) + 'm') $shouldClose = $true } } $this.Write($text) if ($shouldClose) { $this.WriteCsi('0m') } if ($null -ne $link) { $this.EndLink() } } [void] WriteLine([string]$text, [Style]$style) { $this.Write($text, $style, $null) $this.WriteLine() } [void] Style([Style]$style) { if (!$this.Capabilities.Ansi) { return } $codes = [List[byte]]::new() $codes.AddRange([AnsiCodeBuilder]::Build($style.Decoration)) $codes.AddRange([AnsiCodeBuilder]::Build($this.Capabilities.ColorSystem, $style.Foreground, $true)) $codes.AddRange([AnsiCodeBuilder]::Build($this.Capabilities.ColorSystem, $style.Background, $false)) if ($codes.Count -gt 0) { $this.WriteCsi([string]::Join(';', $codes) + 'm') } } [void] ResetStyle() { if ($this.Capabilities.Ansi) { $this.WriteCsi('0m') $this.EndLink() } } [void] BeginLink([AnsiLink]$link) { if ($null -eq $link) { return } if ($this.Capabilities.Ansi -and $this.Capabilities.Links) { $this._linkCount++ $suffix = if ($link.Id.HasValue) { "id=$($link.Id);" } else { '' } $this.Write("`e]8;${suffix}$($link.Url)`e\") } } [void] EndLink() { if ($this.Capabilities.Ansi -and $this.Capabilities.Links -and $this._linkCount -gt 0) { $this._linkCount-- $this.Write("`e]8;;`e\") } } [void] CursorUp([int]$steps) { if ($steps -gt 0) { $this.WriteCsi("${steps}A") } } [void] CursorDown([int]$steps) { if ($steps -gt 0) { $this.WriteCsi("${steps}B") } } [void] CursorHorizontalAbsolute([int]$column) { $this.WriteCsi("${column}G") } [void] EraseInLine([int]$mode = 0) { # 0 = cursor to end, 1 = beginning to cursor, 2 = whole line $this.WriteCsi("${mode}K") } [void] EraseInDisplay([int]$mode = 0) { # 0 = cursor to end, 1 = beginning to cursor, 2 = whole display, 3 = whole display + scrollback $this.WriteCsi("${mode}J") } hidden [void] WriteCsi([string]$parameters) { if ($this.Capabilities.Ansi) { $this.Write("`e[" + $parameters) } } } class AnsiLink { [Nullable[int]]$Id [string]$Url AnsiLink([string]$url) { $this.Url = $url $this.Id = [Random]::new().Next(0, [int]::MaxValue) } } class AnsiMarkupSegment { [string]$Text [Style]$Style [AnsiLink]$Link AnsiMarkupSegment([string]$text, [Style]$style, [AnsiLink]$link) { $this.Text = $text $this.Style = $style $this.Link = $link } } class AnsiMarkup { hidden [AnsiWriter]$_writer AnsiMarkup([AnsiWriter]$writer) { $this._writer = $writer } [void] Write([string]$markup) { $this.Write($markup, [Style]::Plain) } [void] Write([string]$markup, [Style]$style) { foreach ($segment in [AnsiMarkup]::Parse($markup, $style)) { $this._writer.Write($segment.Text, $segment.Style, $segment.Link) } } [void] WriteLine([string]$markup) { $this.Write($markup, [Style]::Plain) $this._writer.WriteLine() } static [List[AnsiMarkupSegment]] Parse([string]$markup, [Style]$style) { $result = [List[AnsiMarkupSegment]]::new() if ($null -eq $style) { $style = [Style]::Plain } foreach ($segment in [MarkupStyleParser]::ParseMarkup($markup, $style)) { $link = if ([string]::IsNullOrWhiteSpace($segment.Link)) { $null } else { [AnsiLink]::new($segment.Link) } $result.Add([AnsiMarkupSegment]::new($segment.Text, $segment.Style, $link)) } return $result } static [Style] ParseTag([string]$tag) { return [MarkupStyleParser]::ParseStyle($tag) } static [string] Escape([string]$markup) { return [MarkupStyleParser]::Escape($markup) } static [string] Remove([string]$markup) { return [MarkupStyleParser]::RemoveMarkup($markup) } static [string] Highlight([string]$markup, [string]$query, [Style]$style) { if ([string]::IsNullOrEmpty($query)) { return $markup } $plainText = [AnsiMarkup]::Remove($markup) $index = $plainText.IndexOf($query, [StringComparison]::OrdinalIgnoreCase) if ($index -lt 0) { return $markup } $prefix = $plainText.Substring(0, $index) $match = $plainText.Substring($index, $query.Length) $suffix = $plainText.Substring($index + $query.Length) $markupStyle = if ($null -ne $style) { $style.ToMarkup() } else { [Style]::Plain.ToMarkup() } if ([string]::IsNullOrWhiteSpace($markupStyle)) { return [AnsiMarkup]::Escape($plainText) } return "{0}[{1}]{2}[/]{3}" -f [AnsiMarkup]::Escape($prefix), $markupStyle, [AnsiMarkup]::Escape($match), [AnsiMarkup]::Escape($suffix) } } |