Private/Console/Widgets.psm1
|
using namespace System using namespace System.Collections.Generic using namespace System.Linq using module ..\Enums.psm1 using module ..\Abstracts.psm1 using module .\Colors.psm1 using module .\Internal.psm1 using module .\Rendering.psm1 using module .\Ansi.psm1 using module .\Boxes.psm1 class Padding { [int]$Left [int]$Top [int]$Right [int]$Bottom Padding([int]$size) { $this.Left = $size $this.Top = $size $this.Right = $size $this.Bottom = $size } Padding([int]$horizontal, [int]$vertical) { $this.Left = $horizontal $this.Top = $vertical $this.Right = $horizontal $this.Bottom = $vertical } Padding([int]$left, [int]$top, [int]$right, [int]$bottom) { $this.Left = $left $this.Top = $top $this.Right = $right $this.Bottom = $bottom } [int] GetLeftSafe() { return [Math]::Max(0, $this.Left) } [int] GetTopSafe() { return [Math]::Max(0, $this.Top) } [int] GetRightSafe() { return [Math]::Max(0, $this.Right) } [int] GetBottomSafe() { return [Math]::Max(0, $this.Bottom) } [int] GetWidth() { return $this.GetLeftSafe() + $this.GetRightSafe() } [int] GetHeight() { return $this.GetTopSafe() + $this.GetBottomSafe() } } class Aligner { static [void] Align([List[Segment]]$segments, [Justify]$alignment, [int]$maxWidth) { if ($null -eq $alignment -or $alignment -eq [Justify]::Left) { return } $width = [Segment]::CellCount($segments) if ($width -ge $maxWidth) { return } switch ($alignment) { ([Justify]::Right) { $diff = $maxWidth - $width $segments.Insert(0, [Segment]::Padding($diff)) } ([Justify]::Center) { $diff = [Math]::Floor(($maxWidth - $width) / 2) $segments.Insert(0, [Segment]::Padding($diff)) $segments.Add([Segment]::Padding($diff)) $remainder = ($maxWidth - $width) % 2 if ($remainder -ne 0) { $segments.Add([Segment]::Padding($remainder)) } } } } static [void] AlignHorizontally([List[Segment]]$segments, [HorizontalAlignment]$alignment, [int]$maxWidth) { $width = [Segment]::CellCount($segments) if ($width -ge $maxWidth) { return } switch ($alignment) { ([HorizontalAlignment]::Left) { $diff = $maxWidth - $width $segments.Add([Segment]::Padding($diff)) } ([HorizontalAlignment]::Right) { $diff = $maxWidth - $width $segments.Insert(0, [Segment]::Padding($diff)) } ([HorizontalAlignment]::Center) { $diff = [Math]::Floor(($maxWidth - $width) / 2) $segments.Insert(0, [Segment]::Padding($diff)) $segments.Add([Segment]::Padding($diff)) $remainder = ($maxWidth - $width) % 2 if ($remainder -ne 0) { $segments.Add([Segment]::Padding($remainder)) } } } } } class Paragraph : IRenderable { hidden [List[SegmentLine]]$_lines [Nullable[Justify]]$Justification [Nullable[Overflow]]$Overflow Paragraph() { $this._lines = [List[SegmentLine]]::new() } Paragraph([string]$text) { $this._lines = [List[SegmentLine]]::new() $this.Append($text, [Style]::Plain, $null) } Paragraph([string]$text, [Style]$style) { $this._lines = [List[SegmentLine]]::new() $this.Append($text, $style, $null) } [int] get_Length() { $len = 0 foreach ($line in $this._lines) { foreach ($s in $line.Segments) { $len += $s.Text.Length } } return $len + [Math]::Max(0, $this._lines.Count - 1) } [int] get_Lines() { return $this._lines.Count } [void] Append([string]$text, [Style]$style, [AnsiLink]$link) { if ([string]::IsNullOrEmpty($text)) { return } if ($null -eq $style) { $style = [Style]::Plain } $first = $true $span = $text.Split("`n") foreach ($lineSpan in $span) { $lineSpan = $lineSpan.TrimEnd("`r") if (!$first -or $this._lines.Count -eq 0) { $line = [SegmentLine]::new() $this._lines.Add($line) } else { $line = $this._lines[$this._lines.Count - 1] } $first = $false if ([string]::IsNullOrEmpty($lineSpan)) { $line.Add([Segment]::Empty) } else { $line.Add([Segment]::new($lineSpan, $style)) } } } [Measurement] Measure([RenderOptions]$options, [int]$maxWidth) { if ($this._lines.Count -eq 0) { return [Measurement]::new(0, 0) } $min = 0 $max = 0 foreach ($line in $this._lines) { # $lineMax = 0 foreach ($seg in $line.Segments) { $cc = $seg.CellCount() if ($cc -gt $min) { $min = $cc } } $lineCc = $line.CellCount() if ($lineCc -gt $max) { $max = $lineCc } } return [Measurement]::new($min, [Math]::Min($max, $maxWidth)) } [Segment[]] Render([RenderOptions]$options, [int]$maxWidth) { if ($this._lines.Count -eq 0) { return [Segment[]]@() } $lines = if ($options.SingleLine) { [List[SegmentLine]]::new($this._lines) } else { $this.SplitLines($maxWidth) } $just = if ($null -ne $options.Justification) { $options.Justification } elseif ($null -ne $this.Justification) { $this.Justification } else { [Justify]::Left } if ($just -ne [Justify]::Left) { foreach ($line in $lines) { [Aligner]::Align($line.Segments, $just, $maxWidth) } } if ($options.SingleLine) { $res = [List[Segment]]::new() foreach ($s in $lines[0].Segments) { if (!$s.IsLineBreak) { $res.Add($s) } } return $res.ToArray() } $outSegments = [List[Segment]]::new() $lineCount = @($lines).Count # Convert to array to get proper count for ($i = 0; $i -lt $lineCount; $i++) { $outSegments.AddRange($lines[$i].Segments) if ($i -lt $lineCount - 1) { $outSegments.Add([Segment]::LineBreak) } } return $outSegments.ToArray() } hidden [List[SegmentLine]] SplitLines([int]$maxWidth) { if ($maxWidth -le 0) { return [List[SegmentLine]]::new() } $lines = [List[SegmentLine]]::new() foreach ($origLine in $this._lines) { $split = [Segment]::SplitLines($origLine.Segments, $maxWidth) $lines.AddRange($split) } return $lines } } class Text : IRenderable { hidden [Paragraph]$_paragraph Text([string]$text) { $this._paragraph = [Paragraph]::new($text) } Text([string]$text, [Style]$style) { $this._paragraph = [Paragraph]::new($text, $style) } [Nullable[Justify]] get_Justification() { return $this._paragraph.Justification } [void] set_Justification([Nullable[Justify]]$val) { $this._paragraph.Justification = $val } [Measurement] Measure([RenderOptions]$options, [int]$maxWidth) { return $this._paragraph.Measure($options, $maxWidth) } [object[]] Render([RenderOptions]$options, [int]$maxWidth) { return $this._paragraph.Render($options, $maxWidth) } } class Markup : IRenderable { hidden [Paragraph]$_paragraph [Nullable[Overflow]]$Overflow [Nullable[Justify]]$Justify Markup([string]$text) { $this.Init($text, [Style]::Plain) } Markup([string]$text, [Style]$style) { $this.Init($text, $style) } hidden [void] Init([string]$text, [Style]$style) { $this._paragraph = [Paragraph]::new() foreach ($segment in [AnsiMarkup]::Parse($text, $style)) { $this._paragraph.Append($segment.Text, $segment.Style, $segment.Link) } } [Nullable[Justify]] get_Justification() { return $this._paragraph.Justification } [void] set_Justification([Nullable[Justify]]$val) { $this._paragraph.Justification = $val } [Measurement] Measure([RenderOptions]$options, [int]$maxWidth) { if ($null -ne $this.Overflow) { $this._paragraph.Overflow = $this.Overflow } if ($null -ne $this.Justify) { $this._paragraph.Justification = $this.Justify } return $this._paragraph.Measure($options, $maxWidth) } [object[]] Render([RenderOptions]$options, [int]$maxWidth) { if ($null -ne $this.Overflow) { $this._paragraph.Overflow = $this.Overflow } if ($null -ne $this.Justify) { $this._paragraph.Justification = $this.Justify } return $this._paragraph.Render($options, $maxWidth) } } class Align : IRenderable { hidden [IRenderable]$_renderable [HorizontalAlignment]$Horizontal [Nullable[VerticalAlignment]]$Vertical [Nullable[int]]$Width [Nullable[int]]$Height Align([IRenderable]$renderable) { $this._renderable = $renderable $this.Horizontal = [HorizontalAlignment]::Left } Align([IRenderable]$renderable, [HorizontalAlignment]$horizontal) { $this._renderable = $renderable $this.Horizontal = $horizontal } Align([IRenderable]$renderable, [HorizontalAlignment]$horizontal, [Nullable[VerticalAlignment]]$vertical) { $this._renderable = $renderable $this.Horizontal = $horizontal $this.Vertical = $vertical } static [Align] Center([IRenderable]$renderable) { return [Align]::new($renderable, [HorizontalAlignment]::Center, $null) } [Measurement] Measure([RenderOptions]$options, [int]$maxWidth) { $targetWidth = if ($null -ne $this.Width) { [Math]::Min([int]$this.Width, $maxWidth) } else { $maxWidth } $measurement = $this._renderable.Measure($options, $targetWidth) return [Measurement]::new([Math]::Min($measurement.Min, $targetWidth), $targetWidth) } [object[]] Render([RenderOptions]$options, [int]$maxWidth) { $targetWidth = if ($null -ne $this.Width) { [Math]::Min([int]$this.Width, $maxWidth) } else { $maxWidth } $rendered = $this._renderable.Render($options, $targetWidth) $lines = [Segment]::SplitLines($rendered, $targetWidth) $targetHeight = if ($null -ne $this.Height) { $this.Height } elseif ($null -ne $options.Height) { $options.Height } else { $null } $blank = [SegmentLine]::new([Segment[]]@([Segment]::new(" " * $targetWidth))) if ($null -ne $this.Vertical -and $null -ne $targetHeight) { switch ($this.Vertical.Value) { ([VerticalAlignment]::Top) { $diff = $targetHeight - $lines.Count for ($i = 0; $i -lt $diff; $i++) { $lines.Add($blank) } } ([VerticalAlignment]::Middle) { $top = [Math]::Floor(($targetHeight - $lines.Count) / 2) $bottom = $targetHeight - $top - $lines.Count for ($i = 0; $i -lt $top; $i++) { $lines.Insert(0, $blank) } for ($i = 0; $i -lt $bottom; $i++) { $lines.Add($blank) } } ([VerticalAlignment]::Bottom) { $diff = $targetHeight - $lines.Count for ($i = 0; $i -lt $diff; $i++) { $lines.Insert(0, $blank) } } } } foreach ($line in $lines) { [Aligner]::AlignHorizontally($line.Segments, $this.Horizontal, $targetWidth) } $outSegments = [List[Segment]]::new() $lineCount = @($lines).Count for ($i = 0; $i -lt $lineCount; $i++) { $outSegments.AddRange($lines[$i].Segments) if ($i -lt $lineCount - 1) { $outSegments.Add([Segment]::LineBreak) } } return $outSegments.ToArray() } } class Padder : IRenderable { hidden [IRenderable]$_child [Padding]$Padding [bool]$Expand Padder([IRenderable]$child) { $this._child = $child $this.Padding = [Padding]::new(1, 1, 1, 1) $this.Expand = $false } Padder([IRenderable]$child, [Padding]$padding) { $this._child = $child $this.Padding = $padding $this.Expand = $false } [Measurement] Measure([RenderOptions]$options, [int]$maxWidth) { $paddingWidth = if ($null -ne $this.Padding) { $this.Padding.GetWidth() } else { 0 } $measurement = $this._child.Measure($options, $maxWidth - $paddingWidth) return [Measurement]::new($measurement.Min + $paddingWidth, $measurement.Max + $paddingWidth) } [object[]] Render([RenderOptions]$options, [int]$maxWidth) { $paddingWidth = if ($null -ne $this.Padding) { $this.Padding.GetWidth() } else { 0 } $childWidth = $maxWidth - $paddingWidth if (!$this.Expand) { $measurement = $this._child.Measure($options, $maxWidth - $paddingWidth) $childWidth = $measurement.Max } $width = $childWidth + $paddingWidth if ($width -gt $maxWidth) { $width = $maxWidth } $result = [List[Segment]]::new() for ($i = 0; $i -lt $this.Padding.Top; $i++) { $result.Add([Segment]::Padding($width)) $result.Add([Segment]::LineBreak) } $childRendered = $this._child.Render($options, $maxWidth - $paddingWidth) foreach ($line in [Segment]::SplitLines($childRendered, $maxWidth - $paddingWidth)) { if ($this.Padding.Left -gt 0) { $result.Add([Segment]::Padding($this.Padding.Left)) } $result.AddRange($line.Segments) if ($this.Padding.Right -gt 0) { $result.Add([Segment]::Padding($this.Padding.Right)) } $lineWidth = $line.CellCount() $diff = $width - $lineWidth - $this.Padding.Left - $this.Padding.Right if ($diff -gt 0) { $result.Add([Segment]::Padding($diff)) } $result.Add([Segment]::LineBreak) } for ($i = 0; $i -lt $this.Padding.Bottom; $i++) { $result.Add([Segment]::Padding($width)) $result.Add([Segment]::LineBreak) } # remove last linebreak if ($result.Count -gt 0 -and $result[$result.Count - 1].IsLineBreak) { $result.RemoveAt($result.Count - 1) } return $result.ToArray() } } class PanelHeader { [string]$Text [Justify]$Justification PanelHeader([string]$text, [Justify]$justification) { $this.Text = $text $this.Justification = $justification } PanelHeader([string]$text) { $this.Text = $text $this.Justification = [Justify]::Left } } class Rule : IRenderable { [string]$Title [Style]$Style [Nullable[Justify]]$Justification [BoxBorder]$Border [int]$TitlePadding [int]$TitleSpacing Rule() { $this.Style = [Style]::Plain $this.Border = [SquareBoxBorder]::new() $this.TitlePadding = 2 $this.TitleSpacing = 1 } Rule([string]$title) { $this.Title = $title $this.Style = [Style]::Plain $this.Border = [SquareBoxBorder]::new() $this.TitlePadding = 2 $this.TitleSpacing = 1 } [Measurement] Measure([RenderOptions]$options, [int]$maxWidth) { return [Measurement]::new($maxWidth, $maxWidth) } [object[]] Render([RenderOptions]$options, [int]$maxWidth) { $extraLength = (2 * $this.TitlePadding) + (2 * $this.TitleSpacing) if ([string]::IsNullOrEmpty($this.Title) -or $maxWidth -le $extraLength) { $safeBorder = [BoxBorder]::GetSafeBorder($options, $this) $text = "" for ($i = 0; $i -lt $maxWidth; $i++) { $text += $safeBorder.GetPart([BoxBorderPart]::Top) } return [Segment[]]@([Segment]::new($text, $this.Style), [Segment]::LineBreak) } $markup = [Markup]::new($this.Title.Trim(), $this.Style) $opt = [RenderOptions]::new() $opt.SingleLine = $true $opt.Ansi = $options.Ansi $opt.ColorSystem = $options.ColorSystem $titleSegs = $markup.Render($opt, $maxWidth - $extraLength) $titleLength = [Segment]::CellCount($titleSegs) $safeBorder = [BoxBorder]::GetSafeBorder($options, $this) $borderPart = $safeBorder.GetPart([BoxBorderPart]::Top) $align = if ($null -ne $this.Justification) { $this.Justification.Value } else { [Justify]::Center } $left = $null $right = $null if ($align -eq [Justify]::Left) { $leftStr = "" for ($i = 0; $i -lt $this.TitlePadding; $i++) { $leftStr += $borderPart } $leftStr += " " * $this.TitleSpacing $left = [Segment]::new($leftStr, $this.Style) $rightLength = $maxWidth - $titleLength - $left.CellCount() - $this.TitleSpacing $rightStr = " " * $this.TitleSpacing for ($i = 0; $i -lt $rightLength; $i++) { $rightStr += $borderPart } $right = [Segment]::new($rightStr, $this.Style) } elseif ($align -eq [Justify]::Center) { $leftLength = [Math]::Floor(($maxWidth - $titleLength) / 2) - $this.TitleSpacing $leftStr = "" for ($i = 0; $i -lt $leftLength; $i++) { $leftStr += $borderPart } $leftStr += " " * $this.TitleSpacing $left = [Segment]::new($leftStr, $this.Style) $rightLength = $maxWidth - $titleLength - $left.CellCount() - $this.TitleSpacing $rightStr = " " * $this.TitleSpacing for ($i = 0; $i -lt $rightLength; $i++) { $rightStr += $borderPart } $right = [Segment]::new($rightStr, $this.Style) } else { $rightStr = " " * $this.TitleSpacing for ($i = 0; $i -lt $this.TitlePadding; $i++) { $rightStr += $borderPart } $right = [Segment]::new($rightStr, $this.Style) $leftLength = $maxWidth - $titleLength - $right.CellCount() - $this.TitleSpacing $leftStr = "" for ($i = 0; $i -lt $leftLength; $i++) { $leftStr += $borderPart } $leftStr += " " * $this.TitleSpacing $left = [Segment]::new($leftStr, $this.Style) } $segments = [List[Segment]]::new() $segments.Add($left) $segments.AddRange($titleSegs) $segments.Add($right) $segments.Add([Segment]::LineBreak) return $segments.ToArray() } } class Panel : IRenderable { hidden [IRenderable]$_child [BoxBorder]$Border [bool]$UseSafeBorder [Style]$BorderStyle [bool]$Expand [Padding]$Padding [PanelHeader]$Header [Nullable[int]]$Width [Nullable[int]]$Height [bool]$Inline Panel([string]$text) { $this.Init([Markup]::new($text)) } Panel([IRenderable]$child) { $this.Init($child) } hidden [void] Init([IRenderable]$child) { $this._child = $child $this.Border = [SquareBoxBorder]::new() $this.UseSafeBorder = $true $this.BorderStyle = [Style]::Plain $this.Expand = $false $this.Padding = [Padding]::new(1, 0, 1, 0) $this.Inline = $false } [Measurement] Measure([RenderOptions]$options, [int]$maxWidth) { $child = [Padder]::new($this._child, $this.Padding) return $this.MeasureChild($options, $maxWidth, $child) } hidden [Measurement] MeasureChild([RenderOptions]$options, [int]$maxWidth, [IRenderable]$child) { $safeBorder = [BoxBorder]::GetSafeBorder($options, $this) $edgeWidth = if ($safeBorder -is [NoBoxBorder]) { 0 } else { 2 } $childWidth = $child.Measure($options, $maxWidth - $edgeWidth) if ($null -ne $this.Width) { $w = $this.Width - $edgeWidth $constrained = [Math]::Min($w, $maxWidth - $edgeWidth) $childWidth = [Measurement]::new([Math]::Min($childWidth.Min, $constrained), $constrained) } return [Measurement]::new($childWidth.Min + $edgeWidth, $childWidth.Max + $edgeWidth) } [object[]] Render([RenderOptions]$options, [int]$maxWidth) { $safeBorder = [BoxBorder]::GetSafeBorder($options, $this) $safeBorderStyle = if ($null -ne $this.BorderStyle) { $this.BorderStyle } else { [Style]::Plain } $showBorder = -not ($safeBorder -is [NoBoxBorder]) $edgeWidth = if ($showBorder) { 2 } else { 0 } $child = [Padder]::new($this._child, $this.Padding) $measure = $this.MeasureChild($options, $maxWidth, $child) $panelWidth = if (!$this.Expand) { [Math]::Min($measure.Max, $maxWidth) } else { $maxWidth } $innerWidth = $panelWidth - $edgeWidth $targetHeight = if ($null -ne $this.Height) { $this.Height - 2 } elseif ($null -ne $options.Height) { $options.Height - 2 } else { $null } if (!$this.Expand -and $null -ne $this.Height) { $targetHeight = $this.Height - 2 } $result = [List[Segment]]::new() if ($showBorder) { $this.AddTopBorder($result, $options, $safeBorder, $safeBorderStyle, $panelWidth) } elseif ($null -ne $this.Header -and -not [string]::IsNullOrEmpty($this.Header.Text)) { $this.AddTopBorder($result, $options, [NoBoxBorder]::new(), $safeBorderStyle, $panelWidth) } $opt = [RenderOptions]::new() $opt.Ansi = $options.Ansi $opt.ColorSystem = $options.ColorSystem $opt.Height = $targetHeight $childSegments = $child.Render($opt, $innerWidth) $lines = [Segment]::SplitLines($childSegments, $innerWidth) $lineCount = @($lines).Count for ($i = 0; $i -lt $lineCount; $i++) { $line = $lines[$i] $isLast = ($i -eq $lineCount - 1) if ($line.Segments.Count -eq 1 -and $line.Segments[0].IsWhiteSpace) { continue } if ($showBorder) { $result.Add([Segment]::new($safeBorder.GetPart([BoxBorderPart]::Left), $safeBorderStyle)) } $result.AddRange($line.Segments) $len = $line.CellCount() if ($len -lt $innerWidth) { $result.Add([Segment]::Padding($innerWidth - $len)) } if ($showBorder) { $result.Add([Segment]::new($safeBorder.GetPart([BoxBorderPart]::Right), $safeBorderStyle)) } $emitLinebreak = -not ($isLast -and !$showBorder -and !$this.Inline) if ($emitLinebreak) { $result.Add([Segment]::LineBreak) } } if ($showBorder) { $result.Add([Segment]::new($safeBorder.GetPart([BoxBorderPart]::BottomLeft), $safeBorderStyle)) $bStr = "" for ($i = 0; $i -lt ($panelWidth - $edgeWidth); $i++) { $bStr += $safeBorder.GetPart([BoxBorderPart]::Bottom) } $result.Add([Segment]::new($bStr, $safeBorderStyle)) $result.Add([Segment]::new($safeBorder.GetPart([BoxBorderPart]::BottomRight), $safeBorderStyle)) } if (!$this.Inline) { $result.Add([Segment]::LineBreak) } return $result.ToArray() } hidden [void] AddTopBorder([List[Segment]]$result, [RenderOptions]$options, [BoxBorder]$border, [Style]$borderStyle, [int]$panelWidth) { $rule = [Rule]::new() $rule.Style = $borderStyle $rule.Border = $border $rule.TitlePadding = 1 $rule.TitleSpacing = 0 if ($null -ne $this.Header) { $rule.Title = $this.Header.Text $rule.Justification = $this.Header.Justification } $result.Add([Segment]::new($border.GetPart([BoxBorderPart]::TopLeft), $borderStyle)) foreach ($seg in $rule.Render($options, $panelWidth - 2)) { if (!$seg.IsLineBreak) { $result.Add($seg) } } $result.Add([Segment]::new($border.GetPart([BoxBorderPart]::TopRight), $borderStyle)) $result.Add([Segment]::LineBreak) } } class TextPath : IRenderable { hidden [string]$_path [Nullable[Justify]]$Justification TextPath([string]$path) { $this._path = $path } [Measurement] Measure([RenderOptions]$options, [int]$maxWidth) { $len = [Cell]::GetCellLength($this._path) $w = [Math]::Min($len, $maxWidth) return [Measurement]::new($w, $w) } [object[]] Render([RenderOptions]$options, [int]$maxWidth) { $markup = [Markup]::new($this._path, [Style]::Plain) $markup.Justification = $this.Justification return $markup.Render($options, $maxWidth) } } class Calendar : IRenderable { [datetime]$Date [datetime[]]$HighlightedDates [Style]$HeaderStyle [Style]$HighlightStyle [Style]$TodayStyle Calendar([datetime]$date) { $this.Date = $date $this.Initialize() } Calendar([datetime]$date, [datetime[]]$highlightedDates) { $this.Date = $date $this.HighlightedDates = $highlightedDates $this.Initialize() } hidden [void] Initialize() { if ($null -eq $this.HighlightedDates) { $this.HighlightedDates = [datetime[]]@() } if ($null -eq $this.HeaderStyle) { $this.HeaderStyle = [Style]::new([Color]::Blue) } if ($null -eq $this.HighlightStyle) { $this.HighlightStyle = [Style]::new([Color]::Yellow) } if ($null -eq $this.TodayStyle) { $this.TodayStyle = [Style]::new([Color]::Green) } } [Measurement] Measure([RenderOptions]$options, [int]$maxWidth) { $width = [Math]::Min(20, $maxWidth) return [Measurement]::new($width, $width) } [object[]] Render([RenderOptions]$options, [int]$maxWidth) { $this.Initialize() $renderWidth = [Math]::Min(20, [Math]::Max(1, $maxWidth)) $segments = [System.Collections.Generic.List[Segment]]::new() $monthLabel = $this.Date.ToString('Y') $header = [Align]::Center([Markup]::new([MarkupStyleParser]::Escape($monthLabel), $this.HeaderStyle)) $segments.AddRange($header.Render($options, $renderWidth)) $segments.Add([Segment]::LineBreak) $segments.AddRange(([Text]::new('Su Mo Tu We Th Fr Sa', $this.HeaderStyle)).Render($options, $renderWidth)) $segments.Add([Segment]::LineBreak) $first = [datetime]::new($this.Date.Year, $this.Date.Month, 1) $daysInMonth = [datetime]::DaysInMonth($this.Date.Year, $this.Date.Month) $today = [datetime]::Today $highlightLookup = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal) foreach ($highlightDate in $this.HighlightedDates) { [void]$highlightLookup.Add($highlightDate.Date.ToString('yyyy-MM-dd')) } $weekSegments = [System.Collections.Generic.List[Segment]]::new() $offset = [int]$first.DayOfWeek for ($i = 0; $i -lt $offset; $i++) { $weekSegments.Add([Segment]::new(' ', [Style]::Plain)) } for ($day = 1; $day -le $daysInMonth; $day++) { $currentDate = [datetime]::new($this.Date.Year, $this.Date.Month, $day) $dateKey = $currentDate.ToString('yyyy-MM-dd') $style = [Style]::Plain if ($highlightLookup.Contains($dateKey)) { $style = $this.HighlightStyle } elseif ($currentDate.Date -eq $today.Date) { $style = $this.TodayStyle } $weekSegments.Add([Segment]::new(('{0,2}' -f $day), $style)) if ((($offset + $day) % 7) -ne 0 -and $day -lt $daysInMonth) { $weekSegments.Add([Segment]::new(' ', [Style]::Plain)) } if ((($offset + $day) % 7) -eq 0 -or $day -eq $daysInMonth) { $segments.AddRange($weekSegments) if ($day -lt $daysInMonth) { $segments.Add([Segment]::LineBreak) } $weekSegments = [System.Collections.Generic.List[Segment]]::new() } } return $segments.ToArray() } } |